Home  >  Article  >  Backend Development  >  How to overcome PHP's naming limitations to model MongoDB operators

How to overcome PHP's naming limitations to model MongoDB operators

DDD
DDDforward
2023-10-18 10:56:00745browse

MongoDB provides drivers for various languages ​​including PHP. To simplify the process of creating aggregation pipelines in PHP, we need to model all stages and operators as functions that can be composed.

Aggregation pipeline is a list of "stage" documents. We'll give an example of querying $match and joining using $lookup:

db.orders.aggregate([
    {
        $match: {
            $or: [
                { status: "shipped" },
                { created_at: { $gte: ISODate("2023-01-01T00:00:00Z") } }
            ]
        }
    },
    {
        $lookup: {
            from: "inventory",
            localField: "product_id",
            foreignField: "product_id",
            as: "inventory_docs"
        }
    }
])

Each key with a dollar prefix is ​​an operator for which we want to provide a factory method.

Namespace functions

The most obvious solution is to create a namespace function, for example: MongoDBOperatoreqof$eq operator.

namespace MongoDB\Operator;
function eq(mixed $value): array {
    return ['$eq' => $value];
}
function lookup(string $from, string $localField, string $foreignField, string $as): array {
    return ['$lookup' => [
        'from' => $from,
        'localField' => $localField,
        'foreignField' => $foreignField,
        'as' => $as,
    ]];
}

Using functions with named parameters, pipes will be written in PHP:

pipeline(
    match(
        or(
            query(status: eq('shipped')),
            query(date: gte(new UTCDateTime())),
        ),
    ),
    lookup(from: 'inventory', localField: 'product_id', foreignField: 'product_id', as: 'inventory_docs'),
);

However, some operator names conflict with reserved keywords in PHP. We cannot create functions (global or namespace) with the following names:

and,

or,

match,

unset,

set,

Add suffix to function name

To avoid the problem of name retention, we can add prefix or suffix to function name.

Suffixed with operator type:

function andQuery(...) { /* ... */ }
function matchStage(...) { /* ... */ }

Underlined:

function _and(...) { /* ... */ }
function _match(...) { /* ... */ }

Or use emoticons. Pretty, but impractical:

function ?and(...) { /* ... */ }
function ?match(...) { /* ... */ }

Static class method

As it happens, the list of reserved keywords for method names is shorter. We can create static methods on classes.

final class Stage {
    public static function lookup(...) { /* ... */ }
    public static function match(...) { /* ... */ }
}
final class Query {
    public static function and(...) { /* ... */ }
    public static function eq(...) { /* ... */ }
}

The writing is a bit long, but it’s still readable.

new Pipeline(
    Stage::match(
        Query::or(
            Query::query(status: Query::eq('shipped')),
            Query::query(date: Query::gte(new UTCDateTime())),
        ),
    ),
    Stage::lookup(from: 'inventory', localField: 'product_id', foreignField: 'product_id', as: 'inventory_docs'),
);

To prevent anyone from creating an instance of this class, we can make the constructor private.

final class Operator {
    // ...
    private function __construct() {} // This constructor cannot be called 
}

We can also use enum without shell. Enum accepts static methods and cannot be instantiated.

enum Query {
    public static function and() { /* ... */ }
    public static function eq() { /* ... */ }
}

Both class and enumeration static methods can be called in the same way.

Closures in variables

Since we couldn’t find an ideal solution, we started to get enthusiastic about unlikely solutions.

If we want a short syntax that looks very similar to MongoDB syntax without name restrictions, then we would think of using variables to store closures. Note that this (...) is the new syntax for creating closures in PHP 8.1.

$eq = Operator::eq(...);
$and = Operator::and(...);

$PHP uses a dollar sign for variable prefixes and MongoDB uses the same operator for prefixes.

pipeline(
    $match(
        $or(
            $query(status: $eq('shipped')),
            $query(date: $gte(new UTCDateTime())),
        ),
    ),
    $lookup(from: 'inventory', localField: 'product_id', foreignField: 'product_id', as: 'inventory_docs'),
);

The library can provide these closures as arrays.

enum Query {
    public static function and(array ...$queries) { /* ... */ }
    public static function eq(mixed $value) { /* ... */ }
    public static function query(mixed ...$query) { /* ... */ }
    /** @return array{and:callable,eq:callable,query:callable} */
    public static function functions(): array {
        return [
            'and' => self::and(...),
            'eq' => self::eq(...),
            'query' => self::query(...),
        ];
    }
}

The syntax for getting all variables is a bit verbose, but still readable.

['and' => $and, 'eq' => $eq, 'query' => $query] = Query::functions();

extract We can import all variables into the current scope using a magical feature in Laravel that is often used but is hated by PHPStorm and static analysis tools.

extract(Query::functions());
var_dump($and(
    $query(foo: $eq(5)),
    $query(bar: $eq(10))
));
// INFO: MixedFunctionCall - Cannot call function on mixed

Conclusion

As you can see, function naming in PHP is not that simple when using reserved keywords.

The above is the detailed content of How to overcome PHP's naming limitations to model MongoDB operators. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:Jérôme TAMARELLE. If there is any infringement, please contact admin@php.cn delete