Skip to content

Latest commit

 

History

History
349 lines (280 loc) · 8.33 KB

File metadata and controls

349 lines (280 loc) · 8.33 KB

Boolean Parameters - The Flag Argument Anti-Pattern

The Problem

Boolean parameters (flag arguments) make code unclear:

  • Unclear intent - What does true mean?
  • Hard to read - create_user('john', 'email@test.com', true, false, true)
    • what?
  • Function does too much - Boolean usually means "do this OR that"
  • Easy to mix up - Which boolean was which?
  • Hard to extend - Adding more booleans makes it worse

Bad Practice (bad.php)

function get_posts($published_only = true) {
    if ($published_only) {
        return get_posts(['post_status' => 'publish']);
    } else {
        return get_posts(['post_status' => 'any']);
    }
}

// Usage - what does true mean?
$posts = get_posts(true);
$all = get_posts(false);

Problems:

  • Reader must look at function definition to understand what true does
  • At call site, get_posts(true) tells you nothing
  • Not self-documenting

Even Worse: Multiple Booleans

create_user('john', 'john@example.com', true, false, true);

What does this mean?

  • First true: Send welcome email? Make admin? Auto-login?
  • False: Don't send email? Not admin? No auto-login?
  • Second true: ???

You have to read the function signature to understand!

Good Practice (good.php)

Solution 1: Separate Functions

function get_published_posts(): array {
    return get_posts( [ 'post_status' => 'publish' ] );
}

function get_all_posts(): array {
    return get_posts( [ 'post_status' => 'any' ] );
}

// Usage - crystal clear!
$posts = get_published_posts();
$all   = get_all_posts();

Benefits:

  • Immediately clear what's happening
  • Self-documenting code
  • Can't call it wrong

Solution 2: Enums (PHP 8.1+)

enum PostFilter: string {
    case PUBLISHED_ONLY = 'publish';
    case DRAFTS_ONLY = 'draft';
    case ALL = 'any';
}

function get_posts( PostFilter $filter = PostFilter::PUBLISHED_ONLY ): array {
    return get_posts( [ 'post_status' => $filter->value ] );
}

// Usage - type-safe and clear
$posts  = get_posts( PostFilter::PUBLISHED_ONLY );
$drafts = get_posts( PostFilter::DRAFTS_ONLY );
$all    = get_posts( PostFilter::ALL );

Benefits:

  • IDE autocomplete shows available options
  • Can't pass invalid values
  • Self-documenting
  • Easy to extend (add more cases)

Solution 3: Builder Pattern

For complex object creation with many options:

$user_id = ( new UserCreator( 'john', 'john@example.com' ) )
    ->with_welcome_email()
    ->with_auto_login()
    ->create();

$admin_id = ( new UserCreator( 'admin', 'admin@example.com' ) )
    ->as_admin()
    ->create();

Benefits:

  • Fluent, readable API
  • Only specify what you need
  • Can't mix up parameter order
  • Easy to extend with new options

Solution 4: Named Arguments (PHP 8.0+)

When you must have optional parameters:

function create_post(
    string $title,
    string $content,
    string $status = 'draft',
    bool $enable_comments = true,
    bool $sticky = false
): int {
    // ...
}

// Usage - named arguments make intent clear
$post_id = create_post(
    title: 'My Post',
    content: 'Content here',
    status: 'publish',
    enable_comments: false,
    sticky: true
);

Benefits:

  • Clear what each argument means
  • Can skip optional parameters
  • Can't mix up order

Solution 5: Options Object

class UserNameDisplayOptions {
    public function __construct(
        public readonly bool $include_email = false,
        public readonly bool $uppercase = false,
        public readonly bool $link_to_profile = false
    ) {}
}

function display_user_name(
    int $user_id,
    UserNameDisplayOptions $options = new UserNameDisplayOptions()
): string {
    // ...
}

// Usage
echo display_user_name(1, new UserNameDisplayOptions(
    include_email: true,
    link_to_profile: true
));

Real-World Examples

WordPress: The wp_mail() Gotcha

// BAD: What's the 4th and 5th parameter?
wp_mail('to@example.com', 'Subject', 'Message', '', '', true);

// The signature is:
// wp_mail($to, $subject, $message, $headers = '', $attachments = [], $more_params)
// That last true? No idea what it does without looking it up!

Better Alternative

class EmailSender {
    private string $to;
    private string $subject;
    private string $message;
    private array $headers = [];
    private array $attachments = [];
    
    public function __construct(string $to, string $subject, string $message) {
        $this->to = $to;
        $this->subject = $subject;
        $this->message = $message;
    }
    
    public function with_headers(array $headers): self {
        $this->headers = $headers;
        return $this;
    }
    
    public function with_attachments(array $attachments): self {
        $this->attachments = $attachments;
        return $this;
    }
    
    public function send(): bool {
        return wp_mail(
            $this->to,
            $this->subject,
            $this->message,
            $this->headers,
            $this->attachments
        );
    }
}

// Usage - clear and fluent
$sent = (new EmailSender('to@example.com', 'Subject', 'Message'))
    ->with_headers(['From: noreply@example.com'])
    ->with_attachments(['/path/to/file.pdf'])
    ->send();

When Boolean Parameters Are OK

Exception 1: Standard Conventions

// OK: Universally understood
sort($array, SORT_DESC); // Not a boolean, but similar
array_unique($array, SORT_REGULAR);

// OK: Common pattern
get_post_meta($post_id, $key, $single = false);

Exception 2: Named Parameters Make It Clear

// OK with named parameters (PHP 8.0+)
$result = search(
    query: 'wordpress',
    case_sensitive: true,
    whole_word: false
);

Exception 3: Private Methods

class PostProcessor {
    public function process_published(): void {
        $this->process(published_only: true);
    }
    
    public function process_all(): void {
        $this->process(published_only: false);
    }
    
    private function process(bool $published_only): void {
        // Private method, called only internally
        // Boolean is OK here
    }
}

Migration Strategy

Before: Boolean Hell

function create_user($user, $email, $send_email = true, $admin = false, $login = false) {
    // 50 lines of mixed logic
}

// Called everywhere:
create_user('john', 'john@test.com', true, false, true);
create_user('admin', 'admin@test.com', false, true, false);

Step 1: Add Named Alternatives

// Keep old function for compatibility
function create_user($user, $email, $send_email = true, $admin = false, $login = false) {
    return (new UserCreator($user, $email))
        ->with_welcome_email($send_email)
        ->as_admin($admin)
        ->with_auto_login($login)
        ->create();
}

// New, better API
class UserCreator {
    // Builder pattern
}

// New code uses builder
$id = (new UserCreator('john', 'email@test.com'))
    ->with_welcome_email()
    ->create();

Step 2: Deprecate Old Function

/**
 * @deprecated Use UserCreator instead
 */
function create_user($user, $email, $send_email = true, $admin = false, $login = false) {
    _deprecated_function(__FUNCTION__, '2.0.0', 'UserCreator');
    // ... implementation
}

Step 3: Eventually Remove

After migration period, remove old function.

Key Takeaways

Use separate functions for different behaviors
Use enums for predefined options (PHP 8.1+)
Use builder pattern for complex object creation
Use named arguments when you must have optional parameters
Use options objects to group related parameters

❌ Don't use boolean flags to change function behavior
❌ Don't have multiple boolean parameters
❌ Don't make callers guess what true means
❌ Don't use booleans for mode selection
❌ Don't use booleans that return different types

The Bottom Line

If you need to look at the function definition to understand what true means, use a different approach.

// BAD: Must look up what true means
delete_post( 123, true );

// GOOD: Obvious what's happening
delete_post_permanently( 123 );

// ALSO GOOD: Enum makes it clear
delete_post( 123, DeletionMode::PERMANENT );

// ALSO GOOD: Named argument
delete_post( 123, permanent: true );

Ask yourself:

  • "Will someone reading this code understand what's happening without looking up the function?"
  • If no → refactor away the boolean parameter!