Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHPLIB-1249 Init code generator project for aggregation builder #1174

Merged
merged 31 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
96e03ea
PHPLIB-1249 Init code generator project
GromNaN Sep 25, 2023
42337fd
Use promoted properties for config validation
GromNaN Sep 25, 2023
e269731
Remove one useless config level
GromNaN Sep 25, 2023
725c5ef
Implement FactoryClassGenerator
GromNaN Sep 25, 2023
32d768f
Move to MongoDB\builder namespace
GromNaN Sep 26, 2023
9f29f28
Add Expression interfaces
GromNaN Sep 26, 2023
194f544
Commit generated classes
GromNaN Sep 26, 2023
900c748
Add minimum for variadic arguments
GromNaN Sep 26, 2023
1ad50f8
Add expression types
GromNaN Sep 27, 2023
2b60cf7
Init BuilderCodec with a unit test
GromNaN Sep 27, 2023
02ae084
Cleanup
GromNaN Sep 27, 2023
d8c3776
Merge multiple arg types
GromNaN Sep 27, 2023
d0ef3bb
Add typed FieldPath classes
GromNaN Sep 28, 2023
c1e2cb4
Pedantry
GromNaN Sep 28, 2023
3fbb7da
Simplify expected pipeline in tests by converting assoc array to objects
GromNaN Sep 28, 2023
eed8ea5
Fix CS
GromNaN Sep 28, 2023
c137337
Remove useless ACCEPTED_TYPES constant. Pass types config to the gene…
GromNaN Sep 28, 2023
1084b1a
Fix psalm issues
GromNaN Sep 28, 2023
a3eef39
Static analysis
GromNaN Sep 29, 2023
63aa368
Add FieldName type
GromNaN Oct 2, 2023
078d679
Reractor operators config to use avoid redondancy
GromNaN Oct 2, 2023
97e0329
Use assert instead of if...throw to validate config
GromNaN Oct 2, 2023
e62d243
Add Aggregation Builder example
GromNaN Oct 2, 2023
fdf8f5a
Implement variadic options
GromNaN Oct 2, 2023
55f8cce
Typo namespace
GromNaN Oct 2, 2023
ace7f60
Assert types is a list of strings
GromNaN Oct 2, 2023
cae32c0
Make generator a single-command app
GromNaN Oct 2, 2023
0e80bb6
Add Optional::Undefined for optional values
GromNaN Oct 2, 2023
d4e53d4
Partial implementation of
GromNaN Oct 2, 2023
c789074
Fix type for $limit
GromNaN Oct 2, 2023
5bf4753
Add object and double to type system
GromNaN Oct 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ tests export-ignore
benchmark export-ignore
docs export-ignore
examples export-ignore
generator export-ignore
mongo-orchestration export-ignore
stubs export-ignore
tools export-ignore
Expand All @@ -14,3 +15,15 @@ phpunit.evergreen.xml export-ignore
phpunit.xml.dist export-ignore
psalm.xml.dist export-ignore
psalm-baseline.xml export-ignore

# Keep generated files from displaying in diffs by default
# https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github
/src/Builder/Aggregation.php linguist-generated=true
/src/Builder/Aggregation/*.php linguist-generated=true
/src/Builder/Expression.php linguist-generated=true
/src/Builder/Expression/*.php linguist-generated=true
/src/Builder/Query.php linguist-generated=true
/src/Builder/Query/*.php linguist-generated=true
/src/Builder/Stage.php linguist-generated=true
/src/Builder/Stage/*.php linguist-generated=true
/src/Builder/Stage/Stage.php linguist-generated=false
82 changes: 82 additions & 0 deletions examples/aggregation-builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);

/**
* This example demonstrates how you can use the builder provided by this library to build an aggregation pipeline.
*/

namespace MongoDB\Examples\AggregationBuilder;

use MongoDB\Builder\Aggregation;
use MongoDB\Builder\BuilderEncoder;
use MongoDB\Builder\Expression;
use MongoDB\Builder\Pipeline;
use MongoDB\Builder\Stage;
use MongoDB\Client;

use function assert;
use function getenv;
use function is_object;
use function MongoDB\BSON\fromPHP;
use function MongoDB\BSON\toRelaxedExtendedJSON;
use function printf;
use function random_int;

require __DIR__ . '/../vendor/autoload.php';

function toJSON(object $document): string
{
return toRelaxedExtendedJSON(fromPHP($document));
}

$client = new Client(getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/');

$collection = $client->test->aggregate;
$collection->drop();

$documents = [];

for ($i = 0; $i < 100; $i++) {
$documents[] = ['randomValue' => random_int(0, 1000)];
}

$collection->insertMany($documents);

$pipeline = new Pipeline(
Stage::group(
_id: null,
totalCount: Aggregation::sum(1),
evenCount: Aggregation::sum(
Aggregation::mod(
Expression::fieldPath('randomValue'),
2,
),
),
oddCount: Aggregation::sum(
Aggregation::subtract(
1,
Aggregation::mod(
Expression::fieldPath('randomValue'),
2,
),
),
),
maxValue: Aggregation::max(
Expression::fieldPath('randomValue'),
),
minValue: Aggregation::min(
Expression::fieldPath('randomValue'),
),
),
);

// @todo Accept a Pipeline instance in Collection::aggregate() and automatically encode it
$encoder = new BuilderEncoder();
$pipeline = $encoder->encode($pipeline);

$cursor = $collection->aggregate($pipeline);

Check failure on line 77 in examples/aggregation-builder.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyInvalidArgument

examples/aggregation-builder.php:77:34: PossiblyInvalidArgument: Argument 1 of MongoDB\Collection::aggregate expects array<array-key, mixed>, but possibly different type array<array-key, mixed>|stdClass|string provided (see https://psalm.dev/092)

foreach ($cursor as $document) {
assert(is_object($document));
printf("%s\n", toJSON($document));
}
18 changes: 18 additions & 0 deletions generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Code Generator for MongoDB PHP Library

This subproject is used to generate the code that is committed to the repository.
The `generator` directory is not included in `mongodb/mongodb` package and is not installed by Composer.

## Contributing

Updating the generated code can be done only by modifying the code generator, or its configuration.

To run the generator, you need to have PHP 8.2+ installed and Composer.

1. Move to the `generator` directory: `cd generator`
1. Install dependencies: `composer install`
1. Run the generator: `./generate`

## Configuration

The `generator/config/*.yaml` files contains the list of operators and stages that are supported by the library.
32 changes: 32 additions & 0 deletions generator/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "mongodb/code-generator",
"type": "project",
"repositories": [
{
"type": "path",
"url": "../",
"symlink": true
}
],
"replace": {
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"require": {
"php": ">=8.2",
"ext-mongodb": "^1.16.0",
"mongodb/mongodb": "@dev",
jmikola marked this conversation as resolved.
Show resolved Hide resolved
"nette/php-generator": "^4",
"symfony/console": "^6.3",
"symfony/yaml": "^6.3"
},
"license": "Apache-2.0",
"autoload": {
"psr-4": {
"MongoDB\\CodeGenerator\\": "src/"
}
},
"config": {
"sort-packages": true
}
}
116 changes: 116 additions & 0 deletions generator/config/expressions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

// Target namespace for the generated files, allows to use ::class notation without use statements

namespace MongoDB\Builder\Expression;

use MongoDB\BSON;
use MongoDB\Model\BSONArray;
use stdClass;

/** @param class-string $resolvesTo */
function typeFieldPath(string $resolvesTo): array
{
return [
'class' => true,
'extends' => FieldPath::class,
'implements' => [$resolvesTo],
'types' => ['string'],
];
}

return [
'null' => ['scalar' => true, 'types' => ['null']],
'number' => ['scalar' => true, 'types' => ['int']],
'decimal' => ['scalar' => true, 'types' => ['float']],
'string' => ['scalar' => true, 'types' => ['string']],
'boolean' => ['scalar' => true, 'types' => ['bool']],

// Use Interface suffix to avoid confusion with MongoDB\Builder\Expression factory class
ExpressionInterface::class => [
'types' => ['mixed'],
jmikola marked this conversation as resolved.
Show resolved Hide resolved
],
// @todo must not start with $
// Allows ORMs to translate field names
FieldName::class => [
'class' => true,
'types' => ['string'],
],
// @todo if replaced by a string, it must start with $
FieldPath::class => [
'class' => true,
'implements' => [ExpressionInterface::class],
'types' => ['string'],
],
// @todo if replaced by a string, it must start with $$
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked by PHPLIB-1265

Variable::class => [
'class' => true,
'implements' => [ExpressionInterface::class],
'types' => ['string'],
],
Literal::class => [
'class' => true,
'implements' => [ExpressionInterface::class],
'types' => ['mixed'],
],
// @todo check for use-case
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked by PHPLIB-1251

ExpressionObject::class => [
'implements' => [ExpressionInterface::class],
'types' => ['array', stdClass::class, BSON\Document::class, BSON\Serializable::class],
],
// @todo check for use-case
Operator::class => [
'implements' => [ExpressionInterface::class],
'types' => ['array', stdClass::class, BSON\Document::class, BSON\Serializable::class],
],
ResolvesToArray::class => [
'implements' => [ExpressionInterface::class],
'types' => ['list', BSONArray::class, BSON\PackedArray::class],
],
ArrayFieldPath::class => typeFieldPath(ResolvesToArray::class),
ResolvesToBool::class => [
'implements' => [ExpressionInterface::class],
'types' => ['bool'],
],
BoolFieldPath::class => typeFieldPath(ResolvesToBool::class),
ResolvesToDate::class => [
'implements' => [ExpressionInterface::class],
'types' => ['DateTimeInterface', 'UTCDateTime'],
],
DateFieldPath::class => typeFieldPath(ResolvesToDate::class),
ResolvesToObject::class => [
'implements' => [ExpressionInterface::class],
'types' => ['array', 'object', BSON\Document::class, BSON\Serializable::class],
],
ObjectFieldPath::class => typeFieldPath(ResolvesToObject::class),
ResolvesToNull::class => [
'implements' => [ExpressionInterface::class],
'types' => ['null'],
],
NullFieldPath::class => typeFieldPath(ResolvesToNull::class),
ResolvesToNumber::class => [
'implements' => [ExpressionInterface::class],
'types' => ['int', 'float', BSON\Int64::class, BSON\Decimal128::class],
],
NumberFieldPath::class => typeFieldPath(ResolvesToNumber::class),
ResolvesToDecimal::class => [
'implements' => [ResolvesToNumber::class],
'types' => ['int', 'float', BSON\Int64::class, BSON\Decimal128::class],
],
DecimalFieldPath::class => typeFieldPath(ResolvesToDecimal::class),
ResolvesToFloat::class => [
'implements' => [ResolvesToNumber::class],
'types' => ['int', 'float', BSON\Int64::class],
],
FloatFieldPath::class => typeFieldPath(ResolvesToFloat::class),
ResolvesToInt::class => [
'implements' => [ResolvesToNumber::class],
'types' => ['int', BSON\Int64::class],
],
IntFieldPath::class => typeFieldPath(ResolvesToInt::class),
ResolvesToString::class => [
'implements' => [ExpressionInterface::class],
'types' => ['string'],
],
StringFieldPath::class => typeFieldPath(ResolvesToString::class),
];
41 changes: 41 additions & 0 deletions generator/config/operators.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace MongoDB\CodeGenerator\Config;

use MongoDB\CodeGenerator\OperatorClassGenerator;
use MongoDB\CodeGenerator\OperatorFactoryGenerator;

return [
// Aggregation Pipeline Stages
[
'configFile' => __DIR__ . '/stages.yaml',
'namespace' => 'MongoDB\\Builder\\Stage',
'classNameSuffix' => 'Stage',
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
],
],
jmikola marked this conversation as resolved.
Show resolved Hide resolved

// Aggregation Pipeline Operators
[
'configFile' => __DIR__ . '/pipeline-operators.yaml',
'namespace' => 'MongoDB\\Builder\\Aggregation',
'classNameSuffix' => 'Aggregation',
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
],
],

// Query Operators
[
'configFile' => __DIR__ . '/query-operators.yaml',
'namespace' => 'MongoDB\\Builder\\Query',
'classNameSuffix' => 'Query',
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
],
],
];
Loading
Loading