WordPress core functions often accept int|string|WP_Post|null which seems flexible but creates problems:
- Defensive programming everywhere - Every function needs to handle all possible input types
- Unclear responsibilities - Who validates? Who converts?
- Runtime errors - Type mismatches only caught when code executes
- Difficult testing - Need to test all type combinations
function display_post_title( $post ) {
// Could be ID, could be post object, could be null...
$post_obj = get_post( $post );
if ( $post_obj ) {
echo $post_obj->post_title;
} else {
echo 'No title';
}
}Problems:
- Function accepts anything (
$posthas no type) - Has to call
get_post()to normalize input - Defensive checks throughout
- Caller doesn't know what to pass
- Multiple responsibilities: validation + display
function display_post_title( WP_Post $post ): void {
echo esc_html( $post->post_title );
}Benefits:
- Crystal clear: expects
WP_Postobject - No validation needed inside
- Single responsibility: display only
- Impossible to call with wrong type
- Easy to test
class PostFactory {
public static function create( int|string|WP_Post|null $input ): ?WP_Post {
if ( $input instanceof WP_Post ) {
return $input;
}
if ( is_numeric( $input ) ) {
$post = get_post( (int) $input );
return $post instanceof WP_Post ? $post : null;
}
// Handle other cases...
return null;
}
}
// Usage: validate ONCE at the boundary
function display_post_safely( $input ): void {
$post = PostFactory::create( $input );
if (!$post) {
echo 'Post not found';
return;
}
// From here on, we work with WP_Post only
display_post_title($post);
}Benefits:
- Validation happens in ONE place (the factory)
- Rest of the code works with strict
WP_Post - Clear separation of concerns
- Easy to add new validation logic
function get_post_author_info($post) {
// Returns WP_User, or null, or false... great!
$author = get_userdata($post->post_author);
return $author ?: false;
}// If author MUST exist, throw exception
function get_post_author(WP_Post $post): WP_User {
$author = get_userdata($post->post_author);
if (!$author instanceof WP_User) {
throw new RuntimeException('Invalid author');
}
return $author;
}
// If author might not exist, use nullable type
function find_post_author(WP_Post $post): ?WP_User {
$author = get_userdata($post->post_author);
return $author instanceof WP_User ? $author : null;
}// Controller/Route - validate input here
function handle_post_request(WP_REST_Request $request): WP_REST_Response {
$post_id = $request->get_param('post_id');
$post = PostFactory::createOrFail($post_id); // Validation boundary
// Service layer - works with strict types
$processor = new PostProcessor($post);
$result = $processor->process();
return new WP_REST_Response($result);
}
// Service layer - no validation, assumes valid input
class PostProcessor {
public function __construct(
private WP_Post $post
) {}
public function process(): array {
// Clean code, no type checking needed
return [
'title' => $this->post->post_title,
'author' => $this->get_author()->display_name,
];
}
private function get_author(): WP_User {
// Strict return type - no null, no false
}
}✅ Use declare(strict_types=1);
✅ Accept ONE specific type per function
✅ Validate at boundaries (controllers, factories)
✅ Internal code uses strict types
✅ Be explicit about nullable (?Type)
✅ Throw exceptions for invalid states
❌ Don't accept mixed unless absolutely necessary
❌ Don't return different types based on success/failure
❌ Don't use false for "not found" - use null or throw
❌ Don't replicate WordPress's mixed-type antipattern
Q: "But WordPress core uses mixed types everywhere!"
A: That doesn't mean YOU should. WordPress maintains backwards compatibility with PHP 5.6 code from 2014. Your modern plugin/theme doesn't have that constraint.
Wrap WordPress functions at your boundaries:
// Your clean API
function get_validated_post(int $post_id): WP_Post {
$post = get_post($post_id); // WordPress function
if (!$post instanceof WP_Post) {
throw new PostNotFoundException($post_id);
}
return $post; // Now strictly typed
}