From 96e03eafdefea73f3ee37f8c9e8eed702c3ba069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 25 Sep 2023 20:48:00 +0200 Subject: [PATCH 01/31] PHPLIB-1249 Init code generator project --- .gitattributes | 1 + generator/README.md | 19 +++ generator/bin/console | 17 +++ generator/composer.json | 31 +++++ generator/config/config.php | 103 +++++++++++++++ generator/config/pipeline-operators.yaml | 42 +++++++ generator/config/query-operators.yaml | 9 ++ generator/config/stages.yaml | 16 +++ generator/src/AbstractGenerator.php | 119 ++++++++++++++++++ generator/src/Command/GenerateCommand.php | 43 +++++++ .../src/Definition/ArgumentDefinition.php | 19 +++ .../src/Definition/GeneratorDefinition.php | 64 ++++++++++ .../src/Definition/OperatorDefinition.php | 24 ++++ generator/src/Definition/YamlReader.php | 29 +++++ generator/src/ValueClassGenerator.php | 61 +++++++++ phpcs.xml.dist | 1 + 16 files changed, 598 insertions(+) create mode 100644 generator/README.md create mode 100755 generator/bin/console create mode 100644 generator/composer.json create mode 100644 generator/config/config.php create mode 100644 generator/config/pipeline-operators.yaml create mode 100644 generator/config/query-operators.yaml create mode 100644 generator/config/stages.yaml create mode 100644 generator/src/AbstractGenerator.php create mode 100644 generator/src/Command/GenerateCommand.php create mode 100644 generator/src/Definition/ArgumentDefinition.php create mode 100644 generator/src/Definition/GeneratorDefinition.php create mode 100644 generator/src/Definition/OperatorDefinition.php create mode 100644 generator/src/Definition/YamlReader.php create mode 100644 generator/src/ValueClassGenerator.php diff --git a/.gitattributes b/.gitattributes index 370c9e0d7..f55e8fa46 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/generator/README.md b/generator/README.md new file mode 100644 index 000000000..0b3a66abf --- /dev/null +++ b/generator/README.md @@ -0,0 +1,19 @@ +# 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 generator code, or it's 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: `bin/console generate` +1. To apply the coding standards of the project, run `vendor/bin/phpcbf` from the root of the repository: `cd .. && vendor/bin/phpcbf` + +## Configuration + +The `generator/config/*.yaml` files contains the list of operations that are supported by the library. diff --git a/generator/bin/console b/generator/bin/console new file mode 100755 index 000000000..b67d0aa6b --- /dev/null +++ b/generator/bin/console @@ -0,0 +1,17 @@ +#!/usr/bin/env php +add(new GenerateCommand(__DIR__ . '/../config/config.php')); +$application->setDefaultCommand('generate'); +$application->run(); diff --git a/generator/composer.json b/generator/composer.json new file mode 100644 index 000000000..213ba130d --- /dev/null +++ b/generator/composer.json @@ -0,0 +1,31 @@ +{ + "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": "*", + "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 + } +} diff --git a/generator/config/config.php b/generator/config/config.php new file mode 100644 index 000000000..1833493fa --- /dev/null +++ b/generator/config/config.php @@ -0,0 +1,103 @@ + [ + [ + // Stage expression classes + 'configFile' => __DIR__ . '/stages.yaml', + 'namespace' => Stage::class, + 'filePath' => $src . '/Aggregation/Stage/', + 'interfaces' => [Stage::class], + 'classNameSuffix' => 'Stage', + ], + [ + // Stage converters + 'configFile' => __DIR__ . '/stages.yaml', + 'generatorClass' => ConverterClassGenerator::class, + 'namespace' => Converter\Stage::class, + 'filePath' => $src . '/Aggregation/Converter/Stage/', + 'parentClass' => AbstractConverter::class, + 'classNameSuffix' => 'StageConverter', + 'supportingNamespace' => Stage::class, + 'supportingClassNameSuffix' => 'Stage', + 'libraryNamespace' => Converter::class, + 'libraryClassName' => 'StageConverter', + ], + [ + // Factory + 'configFile' => __DIR__ . '/stages.yaml', + 'generatorClass' => FactoryClassGenerator::class, + 'className' => 'StageFactory', + 'namespace' => Factory::class, + 'filePath' => $src . '/Aggregation/Factory/', + 'supportingNamespace' => Stage::class, + 'supportingClassNameSuffix' => 'Stage', + ], + ], + 'pipeline-operators' => [ + [ + 'configFile' => __DIR__ . '/pipeline-operators.yaml', + 'generatorClass' => ValueClassGenerator::class, + 'namespace' => MongoDB\Aggregation\PipelineOperator::class, + 'filePath' => $src . '/Aggregation/PipelineOperator/', + 'classNameSuffix' => 'PipelineOperator', + ], + [ + 'configFile' => __DIR__ . '/pipeline-operators.yaml', + 'generatorClass' => ConverterClassGenerator::class, + 'namespace' => Converter\PipelineOperator::class, + 'filePath' => $src . '/Aggregation/Converter/PipelineOperator/', + 'parentClass' => AbstractConverter::class, + 'classNameSuffix' => 'PipelineOperatorConverter', + 'supportingNamespace' => PipelineOperator::class, + 'supportingClassNameSuffix' => 'PipelineOperator', + 'libraryNamespace' => Converter::class, + 'libraryClassName' => 'PipelineOperatorConverter', + ], + [ + // Factory + 'configFile' => __DIR__ . '/pipeline-operators.yaml', + 'generatorClass' => FactoryClassGenerator::class, + 'className' => 'PipelineOperatorFactory', + 'namespace' => Factory::class, + 'filePath' => $src . '/Aggregation/Factory/', + 'supportingNamespace' => PipelineOperator::class, + 'supportingClassNameSuffix' => 'PipelineOperator', + ], + ], + 'query-operators' => [ + [ + 'configFile' => __DIR__ . '/query-operators.yaml', + // These are simple value holders, overwriting is explicitly wanted + 'namespace' => QueryOperator::class, + 'filePath' => $src . '/Aggregation/QueryOperator/', + 'classNameSuffix' => 'QueryOperator', + ], + [ + 'configFile' => __DIR__ . '/query-operators.yaml', + 'generatorClass' => ConverterClassGenerator::class, + 'namespace' => Converter\QueryOperator::class, + 'filePath' => $src . '/Aggregation/Converter/QueryOperator/', + 'parentClass' => AbstractConverter::class, + 'classNameSuffix' => 'QueryOperatorConverter', + 'supportingNamespace' => QueryOperator::class, + 'supportingClassNameSuffix' => 'QueryOperator', + 'libraryNamespace' => Converter::class, + 'libraryClassName' => 'QueryOperatorConverter', + ], + [ + // Factory + 'configFile' => __DIR__ . '/query-operators.yaml', + 'generatorClass' => FactoryClassGenerator::class, + 'className' => 'QueryOperatorFactory', + 'namespace' => Factory::class, + 'filePath' => $src . '/Aggregation/Factory/', + 'supportingNamespace' => QueryOperator::class, + 'supportingClassNameSuffix' => 'QueryOperator', + ], + ], +]; diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml new file mode 100644 index 000000000..5555b533c --- /dev/null +++ b/generator/config/pipeline-operators.yaml @@ -0,0 +1,42 @@ +- name: and + args: + - name: expressions + type: resolvesToExpression + isVariadic: true +- name: eq + args: + - name: expression1 + type: resolvesToExpression + - name: expression2 + type: resolvesToExpression +- name: gt + args: + - name: expression1 + type: resolvesToExpression + - name: expression2 + type: resolvesToExpression +- name: lt + args: + - name: expression1 + type: resolvesToExpression + - name: expression2 + type: resolvesToExpression +- name: ne + args: + - name: expression1 + type: resolvesToExpression + - name: expression2 + type: resolvesToExpression +- name: filter + usesNamedArgs: true + args: + - name: input + type: resolvesToArrayExpression + - name: cond + type: resolvesToBoolExpression + - name: as + type: string + isOptional: true + - name: limit + type: resolvesToNumberExpression + isOptional: true diff --git a/generator/config/query-operators.yaml b/generator/config/query-operators.yaml new file mode 100644 index 000000000..2af65db56 --- /dev/null +++ b/generator/config/query-operators.yaml @@ -0,0 +1,9 @@ +- name: and + args: + - name: query + type: resolvesToQueryOperator + isVariadic: true +- name: expr + args: + - name: expression + type: resolvesToExpression diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml new file mode 100644 index 000000000..901abe6dc --- /dev/null +++ b/generator/config/stages.yaml @@ -0,0 +1,16 @@ +--- +- name: match + args: + - name: matchExpr + type: resolvesToMatchExpression + isVariadic: true + +- name: sort + args: + - name: sortSpecification + type: resolvesToSortSpecification + +- name: limit + args: + - name: limit + type: int diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php new file mode 100644 index 000000000..b00acbfd9 --- /dev/null +++ b/generator/src/AbstractGenerator.php @@ -0,0 +1,119 @@ +> */ + protected array $typeAliases = [ + 'resolvesToExpression' => [Expression\ResolvesToExpression::class, 'array', 'object', 'string', 'int', 'float', 'bool', 'null'], + 'resolvesToArrayExpression' => [Expression\ResolvesToArrayExpression::class, 'array', 'object', 'string'], + 'resolvesToBoolExpression' => [Expression\ResolvesToBoolExpression::class, 'array', 'object', 'string', 'bool'], + 'resolvesToMatchExpression' => ['array', 'object', Expression\ResolvesToMatchExpression::class], + 'resolvesToNumberExpression' => [Expression\ResolvesToBoolExpression::class, 'array', 'object', 'string', 'int', 'float'], + 'resolvesToQueryOperator' => ['array', 'object', Expression\ResolvesToQuery::class], + 'resolvesToSortSpecification' => ['array', 'object', Expression\ResolvesToSortSpecification::class], + ]; + + protected GeneratorDefinition $definition; + protected Printer $printer; + + public function __construct(GeneratorDefinition $definition) + { + $this->definition = $definition; + $this->printer = new PsrPrinter(); + } + + public function createClassesForObjects(array $objects): void + { + array_map( + function ($object): void { + $this->createFileForClass( + $this->definition->filePath, + $this->createClassForObject($object), + ); + }, + $objects, + ); + } + + abstract public function createClassForObject(object $object): ClassType; + + /** @return array{native:string,doc:string} */ + final protected function generateTypeString(ArgumentDefinition $arg): array + { + $type = $arg->type; + $nativeTypes = $this->typeAliases[$type] ?? [$type]; + $docTypes = $nativeTypes; + + foreach ($nativeTypes as $key => $typeName) { + // @todo replace with class_exists + if (str_starts_with($typeName, 'MongoDB\\')) { + $nativeTypes[$key] = $docTypes[$key] = '\\' . $typeName; + + // A union cannot contain both object and a class type, which is redundant and causes a PHP error + if (in_array('object', $nativeTypes, true)) { + unset($nativeTypes[$key]); + } + } + } + + if ($arg->isOptional) { + $nativeTypes[] = 'null'; + $docTypes[] = 'null'; + } + + return [ + 'native' => implode('|', $nativeTypes), + 'doc' => implode('|', $docTypes), + ]; + } + + protected function getClassName(object $object): string + { + return ucfirst($object->name) . $this->definition->classNameSuffix; + } + + protected function createFileForClass(string $dirname, ClassType $class): void + { + $fullName = $dirname . $class->getName() . '.php'; + + $file = new PhpFile(); + $namespace = $file->addNamespace($this->definition->namespace); + $namespace->add($class); + + $this->writeFileFromGenerator($fullName, $file); + } + + protected function writeFileFromGenerator(string $filename, PhpFile $file): void + { + $dirname = dirname($filename); + + $file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); + + if (! is_dir($dirname)) { + mkdir($dirname, 0775, true); + } + + file_put_contents($filename, $this->printer->printFile($file)); + } +} diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php new file mode 100644 index 000000000..9a0d673ea --- /dev/null +++ b/generator/src/Command/GenerateCommand.php @@ -0,0 +1,43 @@ +addOption('force', 'f', null, 'Force generation of all files'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln('Generating code for mongodb/mongodb library'); + + $yamlReader = new YamlReader(); + $config = require $this->configFile; + + // @todo This is a hack to get the first pipeline operator config + $config = $config['pipeline-operators'][0]; + + $config = new GeneratorDefinition($config); + $generatorClass = $config->generatorClass; + $generator = new $generatorClass($config); + $generator->createClassesForObjects($yamlReader->read($config->configFile)); + + return Command::SUCCESS; + } +} diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php new file mode 100644 index 000000000..30056e795 --- /dev/null +++ b/generator/src/Definition/ArgumentDefinition.php @@ -0,0 +1,19 @@ +name = $config['name']; + $this->type = $config['type']; + $this->isOptional = $config['isOptional'] ?? false; + $this->isVariadic = $config['isVariadic'] ?? false; + } +} \ No newline at end of file diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php new file mode 100644 index 000000000..2dfe02b46 --- /dev/null +++ b/generator/src/Definition/GeneratorDefinition.php @@ -0,0 +1,64 @@ + */ + public readonly string $generatorClass; + public readonly string $namespace; + public readonly string $classNameSuffix; + public readonly string $filePath; + public readonly array $interfaces; + public readonly ?string $parentClass; + + public function __construct(array $config) + { + // @todo check required keys and unexpected keys + if (! array_key_exists('generatorClass', $config)) { + throw new InvalidArgumentException('Missing required key "generatorClass"'); + } + + if (! is_subclass_of($config['generatorClass'], AbstractGenerator::class)) { + throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $config['generatorClass'], AbstractGenerator::class)); + } + + if (! array_key_exists('filePath', $config)) { + throw new InvalidArgumentException('Missing required key "filePath"'); + } + + if (! str_ends_with($config['filePath'], '/')) { + throw new InvalidArgumentException(sprintf('File path must end with "/". Got "%s".', $config['filePath'])); + } + + if (! array_key_exists('namespace', $config)) { + throw new InvalidArgumentException('Missing required key "namespace"'); + } + + if (! str_starts_with($config['namespace'], 'MongoDB\\')) { + throw new InvalidArgumentException(sprintf('Namespace must start with "MongoDB\\". Got "%s".', $config['namespace'])); + } + + if (str_ends_with($config['namespace'], '\\')) { + throw new InvalidArgumentException(sprintf('Namespace must not end with "\\". Got "%s".', $config['namespace'])); + } + + $this->configFile = $config['configFile']; + $this->generatorClass = $config['generatorClass']; + $this->namespace = $config['namespace']; + $this->classNameSuffix = $config['classNameSuffix'] ?? ''; + $this->filePath = $config['filePath']; + $this->interfaces = $config['interfaces'] ?? []; + $this->parentClass = $config['parentClass'] ?? null; + } +} diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php new file mode 100644 index 000000000..98c522c26 --- /dev/null +++ b/generator/src/Definition/OperatorDefinition.php @@ -0,0 +1,24 @@ + */ + public array $arguments; + + public function __construct(array $config) + { + $this->name = $config['name']; + $this->usesNamedArgs = $config['usesNamedArgs'] ?? false; + $this->arguments = isset($config['args']) ? array_map( + fn ($arg): ArgumentDefinition => new ArgumentDefinition($arg), + $config['args'], + ) : []; + } +} diff --git a/generator/src/Definition/YamlReader.php b/generator/src/Definition/YamlReader.php new file mode 100644 index 000000000..753b1221f --- /dev/null +++ b/generator/src/Definition/YamlReader.php @@ -0,0 +1,29 @@ +definitions)) { + return $this->definitions[$filename]; + } + + $config = Yaml::parseFile($filename); + + $definitions = []; + foreach ($config as $operator) { + $definitions[] = new OperatorDefinition($operator); + } + + return $definitions; + } +} diff --git a/generator/src/ValueClassGenerator.php b/generator/src/ValueClassGenerator.php new file mode 100644 index 000000000..ec7da7888 --- /dev/null +++ b/generator/src/ValueClassGenerator.php @@ -0,0 +1,61 @@ +getClassName($object)); + + $constuctor = $class->addMethod('__construct') + ->setPrivate() + ->setBody('/* intentionally empty */'); + $constuctorBody = ''; + $constuctorComment = ''; + + foreach ($object->arguments as $argument) { + ['native' => $nativeType, 'doc' => $docType] = $this->generateTypeString($argument); + + // Property + $propertyComment = ''; + $property = $class->addProperty($argument->name); + if ($argument->isVariadic) { + $property->setType('array'); + $propertyComment .= '@param list<' . $docType . '> $' . $argument->name . PHP_EOL; + } else { + $property->setType($nativeType); + } + + $property->setComment($propertyComment); + + // Constructor + $constuctorParam = $constuctor->addParameter($argument->name); + $constuctorParam->setType($nativeType); + if ($argument->isVariadic) { + $constuctor->setVariadic(); + } + + $constuctorComment .= '@param ' . $docType . ' $' . $argument->name . PHP_EOL; + $constuctorBody .= '$this->' . $argument->name . ' = $' . $argument->name . ';' . PHP_EOL; + } + + $constuctor->setComment($constuctorComment); + $constuctor->setBody($constuctorBody); + + return $class; + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 9cee3b66a..1a0254f5b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -13,6 +13,7 @@ src docs/examples examples + generator/src tests tools rector.php From 42337fdea15c6682a4937f5dbadd9f92bc007814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 25 Sep 2023 21:55:23 +0200 Subject: [PATCH 02/31] Use promoted properties for config validation --- generator/bin/console | 3 +- generator/src/AbstractGenerator.php | 1 + generator/src/Command/GenerateCommand.php | 3 +- .../src/Definition/ArgumentDefinition.php | 20 +++---- .../src/Definition/GeneratorDefinition.php | 60 ++++++------------- .../src/Definition/OperatorDefinition.php | 21 ++++--- generator/src/Definition/YamlReader.php | 10 +++- generator/src/ValueClassGenerator.php | 2 +- 8 files changed, 50 insertions(+), 70 deletions(-) diff --git a/generator/bin/console b/generator/bin/console index b67d0aa6b..61902243c 100755 --- a/generator/bin/console +++ b/generator/bin/console @@ -1,5 +1,5 @@ #!/usr/bin/env php -add(new GenerateCommand(__DIR__ . '/../config/config.php')); -$application->setDefaultCommand('generate'); $application->run(); diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index b00acbfd9..b54b66f0e 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -1,4 +1,5 @@ generatorClass; $generator = new $generatorClass($config); $generator->createClassesForObjects($yamlReader->read($config->configFile)); diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index 30056e795..452b23378 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -1,19 +1,15 @@ name = $config['name']; - $this->type = $config['type']; - $this->isOptional = $config['isOptional'] ?? false; - $this->isVariadic = $config['isVariadic'] ?? false; + public function __construct( + public string $name, + public string $type, + public bool $isOptional = false, + public bool $isVariadic = false, + ) { } -} \ No newline at end of file +} diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index 2dfe02b46..48f590bab 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -1,11 +1,11 @@ */ - public readonly string $generatorClass; - public readonly string $namespace; - public readonly string $classNameSuffix; - public readonly string $filePath; - public readonly array $interfaces; - public readonly ?string $parentClass; - - public function __construct(array $config) - { - // @todo check required keys and unexpected keys - if (! array_key_exists('generatorClass', $config)) { - throw new InvalidArgumentException('Missing required key "generatorClass"'); - } - - if (! is_subclass_of($config['generatorClass'], AbstractGenerator::class)) { - throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $config['generatorClass'], AbstractGenerator::class)); - } - - if (! array_key_exists('filePath', $config)) { - throw new InvalidArgumentException('Missing required key "filePath"'); - } - - if (! str_ends_with($config['filePath'], '/')) { - throw new InvalidArgumentException(sprintf('File path must end with "/". Got "%s".', $config['filePath'])); + public function __construct( + public readonly string $configFile, + /** @var class-string */ + public readonly string $generatorClass, + public readonly string $namespace, + public readonly string $filePath, + public readonly string $classNameSuffix = '', + public readonly array $interfaces = [], + public readonly ?string $parentClass = null, + ) { + if (! str_ends_with($this->filePath, '/')) { + throw new InvalidArgumentException(sprintf('File path must end with "/". Got "%s".', $this->filePath)); } - if (! array_key_exists('namespace', $config)) { - throw new InvalidArgumentException('Missing required key "namespace"'); + if (! str_starts_with($this->namespace, 'MongoDB\\')) { + throw new InvalidArgumentException(sprintf('Namespace must start with "MongoDB\\". Got "%s".', $this->namespace)); } - if (! str_starts_with($config['namespace'], 'MongoDB\\')) { - throw new InvalidArgumentException(sprintf('Namespace must start with "MongoDB\\". Got "%s".', $config['namespace'])); + if (str_ends_with($this->namespace, '\\')) { + throw new InvalidArgumentException(sprintf('Namespace must not end with "\\". Got "%s".', $this->namespace)); } - if (str_ends_with($config['namespace'], '\\')) { - throw new InvalidArgumentException(sprintf('Namespace must not end with "\\". Got "%s".', $config['namespace'])); + if (! is_subclass_of($this->generatorClass, AbstractGenerator::class)) { + throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $this->generatorClass, AbstractGenerator::class)); } - - $this->configFile = $config['configFile']; - $this->generatorClass = $config['generatorClass']; - $this->namespace = $config['namespace']; - $this->classNameSuffix = $config['classNameSuffix'] ?? ''; - $this->filePath = $config['filePath']; - $this->interfaces = $config['interfaces'] ?? []; - $this->parentClass = $config['parentClass'] ?? null; } } diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index 98c522c26..aa967afa9 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -1,4 +1,5 @@ */ public array $arguments; - public function __construct(array $config) - { - $this->name = $config['name']; - $this->usesNamedArgs = $config['usesNamedArgs'] ?? false; - $this->arguments = isset($config['args']) ? array_map( - fn ($arg): ArgumentDefinition => new ArgumentDefinition($arg), - $config['args'], - ) : []; + public function __construct( + public string $name, + public bool $usesNamedArgs = false, + array $args = [], + ) { + $this->arguments = array_map( + fn ($arg): ArgumentDefinition => new ArgumentDefinition(...$arg), + $args, + ); } } diff --git a/generator/src/Definition/YamlReader.php b/generator/src/Definition/YamlReader.php index 753b1221f..04795da85 100644 --- a/generator/src/Definition/YamlReader.php +++ b/generator/src/Definition/YamlReader.php @@ -1,16 +1,20 @@ > */ private array $definitions = []; + /** @return list */ public function read(string $filename): array { if (array_key_exists($filename, $this->definitions)) { @@ -18,10 +22,12 @@ public function read(string $filename): array } $config = Yaml::parseFile($filename); + assert(is_array($config)); $definitions = []; foreach ($config as $operator) { - $definitions[] = new OperatorDefinition($operator); + assert(is_array($operator)); + $definitions[] = new OperatorDefinition(...$operator); } return $definitions; diff --git a/generator/src/ValueClassGenerator.php b/generator/src/ValueClassGenerator.php index ec7da7888..d84abbfe6 100644 --- a/generator/src/ValueClassGenerator.php +++ b/generator/src/ValueClassGenerator.php @@ -1,4 +1,5 @@ Date: Mon, 25 Sep 2023 22:12:20 +0200 Subject: [PATCH 03/31] Remove one useless config level --- generator/config/config.php | 192 +++++++++--------- generator/src/AbstractGenerator.php | 4 + generator/src/Command/GenerateCommand.php | 37 +++- .../src/Definition/ArgumentDefinition.php | 2 +- .../src/Definition/GeneratorDefinition.php | 2 +- .../src/Definition/OperatorDefinition.php | 2 +- generator/src/Definition/YamlReader.php | 2 +- 7 files changed, 138 insertions(+), 103 deletions(-) diff --git a/generator/config/config.php b/generator/config/config.php index 1833493fa..2779b900c 100644 --- a/generator/config/config.php +++ b/generator/config/config.php @@ -1,103 +1,111 @@ [ - [ - // Stage expression classes - 'configFile' => __DIR__ . '/stages.yaml', - 'namespace' => Stage::class, - 'filePath' => $src . '/Aggregation/Stage/', - 'interfaces' => [Stage::class], - 'classNameSuffix' => 'Stage', - ], - [ - // Stage converters - 'configFile' => __DIR__ . '/stages.yaml', - 'generatorClass' => ConverterClassGenerator::class, - 'namespace' => Converter\Stage::class, - 'filePath' => $src . '/Aggregation/Converter/Stage/', - 'parentClass' => AbstractConverter::class, - 'classNameSuffix' => 'StageConverter', - 'supportingNamespace' => Stage::class, - 'supportingClassNameSuffix' => 'Stage', - 'libraryNamespace' => Converter::class, - 'libraryClassName' => 'StageConverter', - ], - [ - // Factory - 'configFile' => __DIR__ . '/stages.yaml', - 'generatorClass' => FactoryClassGenerator::class, - 'className' => 'StageFactory', - 'namespace' => Factory::class, - 'filePath' => $src . '/Aggregation/Factory/', - 'supportingNamespace' => Stage::class, - 'supportingClassNameSuffix' => 'Stage', - ], + // Stages + [ + // Stage expression classes + 'configFile' => __DIR__ . '/stages.yaml', + 'generatorClass' => ValueClassGenerator::class, + 'namespace' => MongoDB\Aggregation\Stage::class, + 'filePath' => $src . '/Aggregation/Stage/', + 'interfaces' => [Stage::class], + 'classNameSuffix' => 'Stage', ], - 'pipeline-operators' => [ - [ - 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => ValueClassGenerator::class, - 'namespace' => MongoDB\Aggregation\PipelineOperator::class, - 'filePath' => $src . '/Aggregation/PipelineOperator/', - 'classNameSuffix' => 'PipelineOperator', - ], - [ - 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => ConverterClassGenerator::class, - 'namespace' => Converter\PipelineOperator::class, - 'filePath' => $src . '/Aggregation/Converter/PipelineOperator/', - 'parentClass' => AbstractConverter::class, - 'classNameSuffix' => 'PipelineOperatorConverter', - 'supportingNamespace' => PipelineOperator::class, - 'supportingClassNameSuffix' => 'PipelineOperator', - 'libraryNamespace' => Converter::class, - 'libraryClassName' => 'PipelineOperatorConverter', - ], - [ - // Factory - 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => FactoryClassGenerator::class, - 'className' => 'PipelineOperatorFactory', - 'namespace' => Factory::class, - 'filePath' => $src . '/Aggregation/Factory/', - 'supportingNamespace' => PipelineOperator::class, - 'supportingClassNameSuffix' => 'PipelineOperator', - ], + /* + [ + // Stage converters + 'configFile' => __DIR__ . '/stages.yaml', + 'generatorClass' => ConverterClassGenerator::class, + 'namespace' => Converter\Stage::class, + 'filePath' => $src . '/Aggregation/Converter/Stage/', + 'parentClass' => AbstractConverter::class, + 'classNameSuffix' => 'StageConverter', + 'supportingNamespace' => Stage::class, + 'supportingClassNameSuffix' => 'Stage', + 'libraryNamespace' => Converter::class, + 'libraryClassName' => 'StageConverter', ], - 'query-operators' => [ - [ - 'configFile' => __DIR__ . '/query-operators.yaml', - // These are simple value holders, overwriting is explicitly wanted - 'namespace' => QueryOperator::class, - 'filePath' => $src . '/Aggregation/QueryOperator/', - 'classNameSuffix' => 'QueryOperator', - ], - [ - 'configFile' => __DIR__ . '/query-operators.yaml', - 'generatorClass' => ConverterClassGenerator::class, - 'namespace' => Converter\QueryOperator::class, - 'filePath' => $src . '/Aggregation/Converter/QueryOperator/', - 'parentClass' => AbstractConverter::class, - 'classNameSuffix' => 'QueryOperatorConverter', - 'supportingNamespace' => QueryOperator::class, - 'supportingClassNameSuffix' => 'QueryOperator', - 'libraryNamespace' => Converter::class, - 'libraryClassName' => 'QueryOperatorConverter', - ], - [ - // Factory - 'configFile' => __DIR__ . '/query-operators.yaml', - 'generatorClass' => FactoryClassGenerator::class, - 'className' => 'QueryOperatorFactory', - 'namespace' => Factory::class, - 'filePath' => $src . '/Aggregation/Factory/', - 'supportingNamespace' => QueryOperator::class, - 'supportingClassNameSuffix' => 'QueryOperator', - ], + [ + // Factory + 'configFile' => __DIR__ . '/stages.yaml', + 'generatorClass' => FactoryClassGenerator::class, + 'className' => 'StageFactory', + 'namespace' => Factory::class, + 'filePath' => $src . '/Aggregation/Factory/', + 'supportingNamespace' => Stage::class, + 'supportingClassNameSuffix' => 'Stage', ], + */ + + // Pipeline operators + [ + 'configFile' => __DIR__ . '/pipeline-operators.yaml', + 'generatorClass' => ValueClassGenerator::class, + 'namespace' => MongoDB\Aggregation\PipelineOperator::class, + 'filePath' => $src . '/Aggregation/PipelineOperator/', + 'classNameSuffix' => 'PipelineOperator', + ], + /* + [ + 'configFile' => __DIR__ . '/pipeline-operators.yaml', + 'generatorClass' => ConverterClassGenerator::class, + 'namespace' => Converter\PipelineOperator::class, + 'filePath' => $src . '/Aggregation/Converter/PipelineOperator/', + 'parentClass' => AbstractConverter::class, + 'classNameSuffix' => 'PipelineOperatorConverter', + 'supportingNamespace' => PipelineOperator::class, + 'supportingClassNameSuffix' => 'PipelineOperator', + 'libraryNamespace' => Converter::class, + 'libraryClassName' => 'PipelineOperatorConverter', + ], + [ + // Factory + 'configFile' => __DIR__ . '/pipeline-operators.yaml', + 'generatorClass' => FactoryClassGenerator::class, + 'className' => 'PipelineOperatorFactory', + 'namespace' => Factory::class, + 'filePath' => $src . '/Aggregation/Factory/', + 'supportingNamespace' => PipelineOperator::class, + 'supportingClassNameSuffix' => 'PipelineOperator', + ], + */ + + // Query operators + [ + 'configFile' => __DIR__ . '/query-operators.yaml', + 'generatorClass' => ValueClassGenerator::class, + // These are simple value holders, overwriting is explicitly wanted + 'namespace' => MongoDB\Aggregation\QueryOperator::class, + 'filePath' => $src . '/Aggregation/QueryOperator/', + 'classNameSuffix' => 'QueryOperator', + ], + /* + [ + 'configFile' => __DIR__ . '/query-operators.yaml', + 'generatorClass' => ConverterClassGenerator::class, + 'namespace' => Converter\QueryOperator::class, + 'filePath' => $src . '/Aggregation/Converter/QueryOperator/', + 'parentClass' => AbstractConverter::class, + 'classNameSuffix' => 'QueryOperatorConverter', + 'supportingNamespace' => QueryOperator::class, + 'supportingClassNameSuffix' => 'QueryOperator', + 'libraryNamespace' => Converter::class, + 'libraryClassName' => 'QueryOperatorConverter', + ], + [ + // Factory + 'configFile' => __DIR__ . '/query-operators.yaml', + 'generatorClass' => FactoryClassGenerator::class, + 'className' => 'QueryOperatorFactory', + 'namespace' => Factory::class, + 'filePath' => $src . '/Aggregation/Factory/', + 'supportingNamespace' => QueryOperator::class, + 'supportingClassNameSuffix' => 'QueryOperator', + ], + */ ]; diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index b54b66f0e..7aff85a80 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -18,6 +18,7 @@ use function in_array; use function is_dir; use function mkdir; +use function sort; use function str_starts_with; use function ucfirst; @@ -78,6 +79,9 @@ final protected function generateTypeString(ArgumentDefinition $arg): array } } + sort($nativeTypes); + sort($docTypes); + if ($arg->isOptional) { $nativeTypes[] = 'null'; $docTypes[] = 'null'; diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index 1392a72a7..5eb1f7696 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -10,13 +10,23 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function assert; +use function basename; +use function class_exists; +use function is_array; +use function sprintf; + #[AsCommand(name: 'generate', description: 'Generate code for mongodb/mongodb library')] final class GenerateCommand extends Command { + private YamlReader $yamlReader; + public function __construct( private string $configFile, ) { parent::__construct(); + + $this->yamlReader = new YamlReader(); } public function configure(): void @@ -28,17 +38,30 @@ public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Generating code for mongodb/mongodb library'); - $yamlReader = new YamlReader(); $config = require $this->configFile; + assert(is_array($config)); // @todo This is a hack to get the first pipeline operator config - $config = $config['pipeline-operators'][0]; - - $config = new GeneratorDefinition(...$config); - $generatorClass = $config->generatorClass; - $generator = new $generatorClass($config); - $generator->createClassesForObjects($yamlReader->read($config->configFile)); + foreach ($config as $key => $def) { + assert(is_array($def)); + $this->generate(new GeneratorDefinition(...$def), $output); + } return Command::SUCCESS; } + + private function generate(GeneratorDefinition $definition, OutputInterface $output): void + { + $output->writeln(sprintf('Generating code for %s with %s', basename($definition->configFile), $definition->generatorClass)); + + if (! class_exists($definition->generatorClass)) { + $output->writeln(sprintf('Generator class %s does not exist', $definition->generatorClass)); + + return; + } + + $generatorClass = $definition->generatorClass; + $generator = new $generatorClass($definition); + $generator->createClassesForObjects($this->yamlReader->read($definition->configFile)); + } } diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index 452b23378..2f6c4ab9b 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -3,7 +3,7 @@ namespace MongoDB\CodeGenerator\Definition; -readonly class ArgumentDefinition +final readonly class ArgumentDefinition { public function __construct( public string $name, diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index 48f590bab..b4b521523 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -11,7 +11,7 @@ use function str_ends_with; use function str_starts_with; -class GeneratorDefinition +final readonly class GeneratorDefinition { public function __construct( public readonly string $configFile, diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index aa967afa9..74f174050 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -5,7 +5,7 @@ use function array_map; -readonly class OperatorDefinition +final readonly class OperatorDefinition { /** @var list */ public array $arguments; diff --git a/generator/src/Definition/YamlReader.php b/generator/src/Definition/YamlReader.php index 04795da85..5aae05697 100644 --- a/generator/src/Definition/YamlReader.php +++ b/generator/src/Definition/YamlReader.php @@ -9,7 +9,7 @@ use function assert; use function is_array; -class YamlReader +final class YamlReader { /** @var array> */ private array $definitions = []; From 725c5ef16163fc741165fb881b308a876e24c533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 26 Sep 2023 00:13:31 +0200 Subject: [PATCH 04/31] Implement FactoryClassGenerator --- generator/config/config.php | 52 +++++++++--------- generator/src/AbstractGenerator.php | 23 ++++---- generator/src/FactoryClassGenerator.php | 71 +++++++++++++++++++++++++ generator/src/ValueClassGenerator.php | 13 ++--- 4 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 generator/src/FactoryClassGenerator.php diff --git a/generator/config/config.php b/generator/config/config.php index 2779b900c..ff138f021 100644 --- a/generator/config/config.php +++ b/generator/config/config.php @@ -1,14 +1,16 @@ __DIR__ . '/stages.yaml', 'generatorClass' => ValueClassGenerator::class, 'namespace' => MongoDB\Aggregation\Stage::class, @@ -17,6 +19,7 @@ 'classNameSuffix' => 'Stage', ], /* + // Stage codec [ // Stage converters 'configFile' => __DIR__ . '/stages.yaml', @@ -30,25 +33,23 @@ 'libraryNamespace' => Converter::class, 'libraryClassName' => 'StageConverter', ], + */ + // Stage factory [ - // Factory 'configFile' => __DIR__ . '/stages.yaml', 'generatorClass' => FactoryClassGenerator::class, - 'className' => 'StageFactory', - 'namespace' => Factory::class, - 'filePath' => $src . '/Aggregation/Factory/', - 'supportingNamespace' => Stage::class, - 'supportingClassNameSuffix' => 'Stage', + 'namespace' => Stage::class, + 'filePath' => $src . '/Aggregation/', + 'classNameSuffix' => 'Stage', ], - */ // Pipeline operators [ 'configFile' => __DIR__ . '/pipeline-operators.yaml', 'generatorClass' => ValueClassGenerator::class, - 'namespace' => MongoDB\Aggregation\PipelineOperator::class, - 'filePath' => $src . '/Aggregation/PipelineOperator/', - 'classNameSuffix' => 'PipelineOperator', + 'namespace' => Operator::class, + 'filePath' => $src . '/Aggregation/Operator/', + 'classNameSuffix' => 'Operator', ], /* [ @@ -63,26 +64,23 @@ 'libraryNamespace' => Converter::class, 'libraryClassName' => 'PipelineOperatorConverter', ], + */ [ // Factory 'configFile' => __DIR__ . '/pipeline-operators.yaml', 'generatorClass' => FactoryClassGenerator::class, - 'className' => 'PipelineOperatorFactory', - 'namespace' => Factory::class, - 'filePath' => $src . '/Aggregation/Factory/', - 'supportingNamespace' => PipelineOperator::class, - 'supportingClassNameSuffix' => 'PipelineOperator', + 'namespace' => Operator::class, + 'filePath' => $src . '/Aggregation/', + 'classNameSuffix' => 'Operator', ], - */ // Query operators [ 'configFile' => __DIR__ . '/query-operators.yaml', 'generatorClass' => ValueClassGenerator::class, - // These are simple value holders, overwriting is explicitly wanted - 'namespace' => MongoDB\Aggregation\QueryOperator::class, - 'filePath' => $src . '/Aggregation/QueryOperator/', - 'classNameSuffix' => 'QueryOperator', + 'namespace' => QueryOperator::class, + 'filePath' => $src . '/Query/Operator/', + 'classNameSuffix' => 'Operator', ], /* [ @@ -97,15 +95,13 @@ 'libraryNamespace' => Converter::class, 'libraryClassName' => 'QueryOperatorConverter', ], + */ [ // Factory 'configFile' => __DIR__ . '/query-operators.yaml', 'generatorClass' => FactoryClassGenerator::class, - 'className' => 'QueryOperatorFactory', - 'namespace' => Factory::class, - 'filePath' => $src . '/Aggregation/Factory/', - 'supportingNamespace' => QueryOperator::class, - 'supportingClassNameSuffix' => 'QueryOperator', + 'namespace' => QueryOperator::class, + 'filePath' => $src . '/Query/', + 'classNameSuffix' => 'Operator', ], - */ ]; diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index 7aff85a80..1857e6e10 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -3,6 +3,7 @@ namespace MongoDB\CodeGenerator; +use InvalidArgumentException; use MongoDB\Aggregation\Expression; use MongoDB\CodeGenerator\Definition\ArgumentDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; @@ -41,21 +42,25 @@ abstract class AbstractGenerator public function __construct(GeneratorDefinition $definition) { + $this->validate($definition); + $this->definition = $definition; $this->printer = new PsrPrinter(); } + /** @throws InvalidArgumentException when definition is invalid */ + protected function validate(GeneratorDefinition $definition): void + { + } + public function createClassesForObjects(array $objects): void { - array_map( - function ($object): void { - $this->createFileForClass( - $this->definition->filePath, - $this->createClassForObject($object), - ); - }, - $objects, - ); + foreach ($objects as $object) { + $this->createFileForClass( + $this->definition->filePath, + $this->createClassForObject($object), + ); + } } abstract public function createClassForObject(object $object): ClassType; diff --git a/generator/src/FactoryClassGenerator.php b/generator/src/FactoryClassGenerator.php new file mode 100644 index 000000000..180401ba4 --- /dev/null +++ b/generator/src/FactoryClassGenerator.php @@ -0,0 +1,71 @@ +createFileForClass( + $this->definition->filePath, + $this->createBuilderClass($objects), + ); + } + + private function createBuilderClass(array $objects): ClassType + { + // We use the namespace as class name + $namespaceParts = explode('\\', $this->definition->namespace); + $className = array_pop($namespaceParts); + $namespace = implode('\\', $namespaceParts); + + $class = new ClassType($className, new PhpNamespace($namespace)); + $class->setFinal(); + + foreach ($objects as $object) { + assert($object instanceof OperatorDefinition); + $operatorClassName = '\\' . $this->definition->namespace . '\\' . $this->getClassName($object); + + $method = $class->addMethod($object->name); + $method->setStatic(); + $method->addBody('return new ' . $operatorClassName . '('); + foreach ($object->arguments as $argument) { + ['native' => $nativeType, 'doc' => $docType] = $this->generateTypeString($argument); + + $parameter = $method->addParameter($argument->name); + $parameter->setType($nativeType); + if ($argument->isVariadic) { + $method->setVariadic(); + } + + $method->addComment('@param ' . $docType . ' $' . $argument->name); + $method->addBody(' $' . $argument->name . ','); + } + + $method->addBody(');'); + $method->setReturnType($operatorClassName); + + $method->addComment('@return ' . $operatorClassName); + } + + return $class; + } + + public function createClassForObject(object $object): ClassType + { + // Not used + } +} diff --git a/generator/src/ValueClassGenerator.php b/generator/src/ValueClassGenerator.php index d84abbfe6..3f31185cb 100644 --- a/generator/src/ValueClassGenerator.php +++ b/generator/src/ValueClassGenerator.php @@ -21,11 +21,7 @@ public function createClassForObject(object $object): ClassType $class = new ClassType($this->getClassName($object)); - $constuctor = $class->addMethod('__construct') - ->setPrivate() - ->setBody('/* intentionally empty */'); - $constuctorBody = ''; - $constuctorComment = ''; + $constuctor = $class->addMethod('__construct'); foreach ($object->arguments as $argument) { ['native' => $nativeType, 'doc' => $docType] = $this->generateTypeString($argument); @@ -49,13 +45,10 @@ public function createClassForObject(object $object): ClassType $constuctor->setVariadic(); } - $constuctorComment .= '@param ' . $docType . ' $' . $argument->name . PHP_EOL; - $constuctorBody .= '$this->' . $argument->name . ' = $' . $argument->name . ';' . PHP_EOL; + $constuctor->addComment('@param ' . $docType . ' $' . $argument->name . PHP_EOL); + $constuctor->addBody('$this->' . $argument->name . ' = $' . $argument->name . ';' . PHP_EOL); } - $constuctor->setComment($constuctorComment); - $constuctor->setBody($constuctorBody); - return $class; } } From 32d768f19a7d43c5b4ceafe4c2a819ba9b82cef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 26 Sep 2023 12:12:06 +0200 Subject: [PATCH 05/31] Move to MongoDB\builder namespace --- generator/config/config.php | 92 +++++-------------- generator/src/AbstractGenerator.php | 9 +- .../src/Definition/GeneratorDefinition.php | 4 +- generator/src/FactoryClassGenerator.php | 28 +++--- generator/src/ValueClassGenerator.php | 6 +- 5 files changed, 45 insertions(+), 94 deletions(-) diff --git a/generator/config/config.php b/generator/config/config.php index ff138f021..a0da4ade5 100644 --- a/generator/config/config.php +++ b/generator/config/config.php @@ -1,107 +1,59 @@ __DIR__ . '/stages.yaml', 'generatorClass' => ValueClassGenerator::class, - 'namespace' => MongoDB\Aggregation\Stage::class, - 'filePath' => $src . '/Aggregation/Stage/', - 'interfaces' => [Stage::class], + 'namespace' => 'MongoDB\\Builder\\Stage', + 'filePath' => $src . '/Builder/Stage', 'classNameSuffix' => 'Stage', ], - /* - // Stage codec - [ - // Stage converters - 'configFile' => __DIR__ . '/stages.yaml', - 'generatorClass' => ConverterClassGenerator::class, - 'namespace' => Converter\Stage::class, - 'filePath' => $src . '/Aggregation/Converter/Stage/', - 'parentClass' => AbstractConverter::class, - 'classNameSuffix' => 'StageConverter', - 'supportingNamespace' => Stage::class, - 'supportingClassNameSuffix' => 'Stage', - 'libraryNamespace' => Converter::class, - 'libraryClassName' => 'StageConverter', - ], - */ - // Stage factory [ 'configFile' => __DIR__ . '/stages.yaml', 'generatorClass' => FactoryClassGenerator::class, - 'namespace' => Stage::class, - 'filePath' => $src . '/Aggregation/', + 'namespace' => 'MongoDB\\Builder\\Stage', + 'filePath' => $src . '/Builder/Stage', 'classNameSuffix' => 'Stage', ], - // Pipeline operators + // Aggregation Pipeline Operators [ 'configFile' => __DIR__ . '/pipeline-operators.yaml', 'generatorClass' => ValueClassGenerator::class, - 'namespace' => Operator::class, - 'filePath' => $src . '/Aggregation/Operator/', - 'classNameSuffix' => 'Operator', - ], - /* - [ - 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => ConverterClassGenerator::class, - 'namespace' => Converter\PipelineOperator::class, - 'filePath' => $src . '/Aggregation/Converter/PipelineOperator/', - 'parentClass' => AbstractConverter::class, - 'classNameSuffix' => 'PipelineOperatorConverter', - 'supportingNamespace' => PipelineOperator::class, - 'supportingClassNameSuffix' => 'PipelineOperator', - 'libraryNamespace' => Converter::class, - 'libraryClassName' => 'PipelineOperatorConverter', + 'namespace' => 'MongoDB\\Builder\\Aggregation', + 'filePath' => $src . '/Builder/Aggregation', + 'classNameSuffix' => 'Aggregation', ], - */ [ - // Factory 'configFile' => __DIR__ . '/pipeline-operators.yaml', 'generatorClass' => FactoryClassGenerator::class, - 'namespace' => Operator::class, - 'filePath' => $src . '/Aggregation/', - 'classNameSuffix' => 'Operator', + 'namespace' => 'MongoDB\\Builder\\Aggregation', + 'filePath' => $src . '/Builder/Aggregation', + 'classNameSuffix' => 'Aggregation', ], - // Query operators + // Query Operators [ 'configFile' => __DIR__ . '/query-operators.yaml', 'generatorClass' => ValueClassGenerator::class, - 'namespace' => QueryOperator::class, - 'filePath' => $src . '/Query/Operator/', - 'classNameSuffix' => 'Operator', - ], - /* - [ - 'configFile' => __DIR__ . '/query-operators.yaml', - 'generatorClass' => ConverterClassGenerator::class, - 'namespace' => Converter\QueryOperator::class, - 'filePath' => $src . '/Aggregation/Converter/QueryOperator/', - 'parentClass' => AbstractConverter::class, - 'classNameSuffix' => 'QueryOperatorConverter', - 'supportingNamespace' => QueryOperator::class, - 'supportingClassNameSuffix' => 'QueryOperator', - 'libraryNamespace' => Converter::class, - 'libraryClassName' => 'QueryOperatorConverter', + 'namespace' => 'MongoDB\\Builder\\Query', + 'filePath' => $src . '/Builder/Query', + 'classNameSuffix' => 'Query', ], - */ [ - // Factory 'configFile' => __DIR__ . '/query-operators.yaml', 'generatorClass' => FactoryClassGenerator::class, - 'namespace' => QueryOperator::class, - 'filePath' => $src . '/Query/', - 'classNameSuffix' => 'Operator', + 'namespace' => 'MongoDB\\Builder\\Query', + 'filePath' => $src . '/Builder/Query', + 'classNameSuffix' => 'Query', ], ]; diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index 1857e6e10..1b5cd7c7d 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -4,7 +4,7 @@ namespace MongoDB\CodeGenerator; use InvalidArgumentException; -use MongoDB\Aggregation\Expression; +use MongoDB\Builder\Expression; use MongoDB\CodeGenerator\Definition\ArgumentDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use Nette\PhpGenerator\ClassType; @@ -12,7 +12,6 @@ use Nette\PhpGenerator\Printer; use Nette\PhpGenerator\PsrPrinter; -use function array_map; use function dirname; use function file_put_contents; use function implode; @@ -103,12 +102,12 @@ protected function getClassName(object $object): string return ucfirst($object->name) . $this->definition->classNameSuffix; } - protected function createFileForClass(string $dirname, ClassType $class): void + protected function createFileForClass(string $dirname, ClassType $class, ?string $namespace = null): void { - $fullName = $dirname . $class->getName() . '.php'; + $fullName = $dirname . '/' . $class->getName() . '.php'; $file = new PhpFile(); - $namespace = $file->addNamespace($this->definition->namespace); + $namespace = $file->addNamespace($namespace ?? $this->definition->namespace); $namespace->add($class); $this->writeFileFromGenerator($fullName, $file); diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index b4b521523..94bba5bbe 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -23,8 +23,8 @@ public function __construct( public readonly array $interfaces = [], public readonly ?string $parentClass = null, ) { - if (! str_ends_with($this->filePath, '/')) { - throw new InvalidArgumentException(sprintf('File path must end with "/". Got "%s".', $this->filePath)); + if (str_ends_with($this->filePath, '/')) { + throw new InvalidArgumentException(sprintf('File path must not end with "/". Got "%s".', $this->filePath)); } if (! str_starts_with($this->namespace, 'MongoDB\\')) { diff --git a/generator/src/FactoryClassGenerator.php b/generator/src/FactoryClassGenerator.php index 180401ba4..cfa4a3a08 100644 --- a/generator/src/FactoryClassGenerator.php +++ b/generator/src/FactoryClassGenerator.php @@ -6,34 +6,35 @@ use MongoDB\CodeGenerator\Definition\OperatorDefinition; use Nette\PhpGenerator\ClassType; -use Nette\PhpGenerator\PhpNamespace; use function array_pop; use function assert; +use function dirname; use function explode; use function implode; -use const PHP_EOL; - /** @internal */ final class FactoryClassGenerator extends AbstractGenerator { public function createClassesForObjects(array $objects): void - { - $this->createFileForClass( - $this->definition->filePath, - $this->createBuilderClass($objects), - ); - } - - private function createBuilderClass(array $objects): ClassType { // We use the namespace as class name $namespaceParts = explode('\\', $this->definition->namespace); $className = array_pop($namespaceParts); $namespace = implode('\\', $namespaceParts); - $class = new ClassType($className, new PhpNamespace($namespace)); + $this->createFileForClass( + dirname($this->definition->filePath), + $this->createFactoryClass($objects, $className), + $namespace, + ); + } + + private function createFactoryClass(array $objects, string $className): ClassType + { + $class = new ClassType($className); $class->setFinal(); + $class->addMethod('__construct')->setPrivate() + ->setComment('This class cannot be instantiated.'); foreach ($objects as $object) { assert($object instanceof OperatorDefinition); @@ -56,9 +57,8 @@ private function createBuilderClass(array $objects): ClassType } $method->addBody(');'); - $method->setReturnType($operatorClassName); - $method->addComment('@return ' . $operatorClassName); + $method->setReturnType($operatorClassName); } return $class; diff --git a/generator/src/ValueClassGenerator.php b/generator/src/ValueClassGenerator.php index 3f31185cb..cfa0ec559 100644 --- a/generator/src/ValueClassGenerator.php +++ b/generator/src/ValueClassGenerator.php @@ -31,7 +31,7 @@ public function createClassForObject(object $object): ClassType $property = $class->addProperty($argument->name); if ($argument->isVariadic) { $property->setType('array'); - $propertyComment .= '@param list<' . $docType . '> $' . $argument->name . PHP_EOL; + $propertyComment .= '@param list<' . $docType . '> $' . $argument->name; } else { $property->setType($nativeType); } @@ -45,8 +45,8 @@ public function createClassForObject(object $object): ClassType $constuctor->setVariadic(); } - $constuctor->addComment('@param ' . $docType . ' $' . $argument->name . PHP_EOL); - $constuctor->addBody('$this->' . $argument->name . ' = $' . $argument->name . ';' . PHP_EOL); + $constuctor->addComment('@param ' . $docType . ' $' . $argument->name); + $constuctor->addBody('$this->' . $argument->name . ' = $' . $argument->name . ';'); } return $class; From 9f29f289973f48dafcb4715ebbcc0640bff4c083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 26 Sep 2023 14:06:49 +0200 Subject: [PATCH 06/31] Add Expression interfaces --- generator/composer.json | 1 + generator/config/pipeline-operators.yaml | 6 ++++++ generator/config/query-operators.yaml | 2 ++ generator/config/stages.yaml | 3 --- generator/src/AbstractGenerator.php | 9 ++++----- .../src/Definition/OperatorDefinition.php | 1 + generator/src/ValueClassGenerator.php | 20 +++++++++++++++++-- .../Expression/ResolvesToArrayExpression.php | 8 ++++++++ .../Expression/ResolvesToBoolExpression.php | 8 ++++++++ .../Expression/ResolvesToExpression.php | 8 ++++++++ .../Expression/ResolvesToMatchExpression.php | 8 ++++++++ .../Expression/ResolvesToNumberExpression.php | 8 ++++++++ .../Expression/ResolvesToQueryOperator.php | 8 ++++++++ .../ResolvesToSortSpecification.php | 8 ++++++++ 14 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 src/Builder/Expression/ResolvesToArrayExpression.php create mode 100644 src/Builder/Expression/ResolvesToBoolExpression.php create mode 100644 src/Builder/Expression/ResolvesToExpression.php create mode 100644 src/Builder/Expression/ResolvesToMatchExpression.php create mode 100644 src/Builder/Expression/ResolvesToNumberExpression.php create mode 100644 src/Builder/Expression/ResolvesToQueryOperator.php create mode 100644 src/Builder/Expression/ResolvesToSortSpecification.php diff --git a/generator/composer.json b/generator/composer.json index 213ba130d..faa365acf 100644 --- a/generator/composer.json +++ b/generator/composer.json @@ -15,6 +15,7 @@ "require": { "php": ">=8.2", "ext-mongodb": "*", + "mongodb/mongodb": "@dev", "nette/php-generator": "^4", "symfony/console": "^6.3", "symfony/yaml": "^6.3" diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index 5555b533c..43a01c731 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -1,33 +1,39 @@ - name: and + type: resolvesToBoolExpression args: - name: expressions type: resolvesToExpression isVariadic: true - name: eq + type: resolvesToBoolExpression args: - name: expression1 type: resolvesToExpression - name: expression2 type: resolvesToExpression - name: gt + type: resolvesToBoolExpression args: - name: expression1 type: resolvesToExpression - name: expression2 type: resolvesToExpression - name: lt + type: resolvesToBoolExpression args: - name: expression1 type: resolvesToExpression - name: expression2 type: resolvesToExpression - name: ne + type: resolvesToBoolExpression args: - name: expression1 type: resolvesToExpression - name: expression2 type: resolvesToExpression - name: filter + type: resolvesToArrayExpression usesNamedArgs: true args: - name: input diff --git a/generator/config/query-operators.yaml b/generator/config/query-operators.yaml index 2af65db56..c217b7a77 100644 --- a/generator/config/query-operators.yaml +++ b/generator/config/query-operators.yaml @@ -1,9 +1,11 @@ - name: and + type: resolvesToBoolExpression args: - name: query type: resolvesToQueryOperator isVariadic: true - name: expr + type: resolvesToExpression args: - name: expression type: resolvesToExpression diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index 901abe6dc..7c438f567 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -1,15 +1,12 @@ ---- - name: match args: - name: matchExpr type: resolvesToMatchExpression isVariadic: true - - name: sort args: - name: sortSpecification type: resolvesToSortSpecification - - name: limit args: - name: limit diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index 1b5cd7c7d..2da0f70d9 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -16,10 +16,10 @@ use function file_put_contents; use function implode; use function in_array; +use function interface_exists; use function is_dir; use function mkdir; use function sort; -use function str_starts_with; use function ucfirst; /** @internal */ @@ -32,7 +32,7 @@ abstract class AbstractGenerator 'resolvesToBoolExpression' => [Expression\ResolvesToBoolExpression::class, 'array', 'object', 'string', 'bool'], 'resolvesToMatchExpression' => ['array', 'object', Expression\ResolvesToMatchExpression::class], 'resolvesToNumberExpression' => [Expression\ResolvesToBoolExpression::class, 'array', 'object', 'string', 'int', 'float'], - 'resolvesToQueryOperator' => ['array', 'object', Expression\ResolvesToQuery::class], + 'resolvesToQueryOperator' => ['array', 'object', Expression\ResolvesToQueryOperator::class], 'resolvesToSortSpecification' => ['array', 'object', Expression\ResolvesToSortSpecification::class], ]; @@ -72,11 +72,10 @@ final protected function generateTypeString(ArgumentDefinition $arg): array $docTypes = $nativeTypes; foreach ($nativeTypes as $key => $typeName) { - // @todo replace with class_exists - if (str_starts_with($typeName, 'MongoDB\\')) { + if (interface_exists($typeName)) { $nativeTypes[$key] = $docTypes[$key] = '\\' . $typeName; - // A union cannot contain both object and a class type, which is redundant and causes a PHP error + // @todo replace "object" with "stdClass" and force any class object to implement the proper interface if (in_array('object', $nativeTypes, true)) { unset($nativeTypes[$key]); } diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index 74f174050..f9537ab0e 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -12,6 +12,7 @@ public function __construct( public string $name, + public ?string $type = null, public bool $usesNamedArgs = false, array $args = [], ) { diff --git a/generator/src/ValueClassGenerator.php b/generator/src/ValueClassGenerator.php index cfa0ec559..4412f6263 100644 --- a/generator/src/ValueClassGenerator.php +++ b/generator/src/ValueClassGenerator.php @@ -5,10 +5,11 @@ use MongoDB\CodeGenerator\Definition\OperatorDefinition; use Nette\PhpGenerator\ClassType; +use RuntimeException; use function assert; - -use const PHP_EOL; +use function interface_exists; +use function ucfirst; /** * Generates a value object class for stages and operators. @@ -20,6 +21,7 @@ public function createClassForObject(object $object): ClassType assert($object instanceof OperatorDefinition); $class = new ClassType($this->getClassName($object)); + $class->setImplements($this->getInterfaces($object)); $constuctor = $class->addMethod('__construct'); @@ -51,4 +53,18 @@ public function createClassForObject(object $object): ClassType return $class; } + + private function getInterfaces(OperatorDefinition $definition): array + { + if ($definition->type === null) { + return []; + } + + $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($definition->type); + if (! interface_exists($interface)) { + throw new RuntimeException('Interface ' . $interface . ' does not exist'); + } + + return [$interface]; + } } diff --git a/src/Builder/Expression/ResolvesToArrayExpression.php b/src/Builder/Expression/ResolvesToArrayExpression.php new file mode 100644 index 000000000..eccb127bd --- /dev/null +++ b/src/Builder/Expression/ResolvesToArrayExpression.php @@ -0,0 +1,8 @@ + Date: Tue, 26 Sep 2023 14:08:08 +0200 Subject: [PATCH 07/31] Commit generated classes --- src/Builder/Aggregation.php | 110 ++++++++++++++++++ src/Builder/Aggregation/AndAggregation.php | 22 ++++ src/Builder/Aggregation/EqAggregation.php | 28 +++++ src/Builder/Aggregation/FilterAggregation.php | 35 ++++++ src/Builder/Aggregation/GtAggregation.php | 28 +++++ src/Builder/Aggregation/LtAggregation.php | 28 +++++ src/Builder/Aggregation/NeAggregation.php | 28 +++++ src/Builder/Query.php | 38 ++++++ src/Builder/Query/AndQuery.php | 22 ++++ src/Builder/Query/ExprQuery.php | 20 ++++ src/Builder/Stage.php | 46 ++++++++ src/Builder/Stage/LimitStage.php | 17 +++ src/Builder/Stage/MatchStage.php | 21 ++++ src/Builder/Stage/SortStage.php | 20 ++++ 14 files changed, 463 insertions(+) create mode 100644 src/Builder/Aggregation.php create mode 100644 src/Builder/Aggregation/AndAggregation.php create mode 100644 src/Builder/Aggregation/EqAggregation.php create mode 100644 src/Builder/Aggregation/FilterAggregation.php create mode 100644 src/Builder/Aggregation/GtAggregation.php create mode 100644 src/Builder/Aggregation/LtAggregation.php create mode 100644 src/Builder/Aggregation/NeAggregation.php create mode 100644 src/Builder/Query.php create mode 100644 src/Builder/Query/AndQuery.php create mode 100644 src/Builder/Query/ExprQuery.php create mode 100644 src/Builder/Stage.php create mode 100644 src/Builder/Stage/LimitStage.php create mode 100644 src/Builder/Stage/MatchStage.php create mode 100644 src/Builder/Stage/SortStage.php diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php new file mode 100644 index 000000000..4226e5a3d --- /dev/null +++ b/src/Builder/Aggregation.php @@ -0,0 +1,110 @@ + $expressions */ + public array $expressions; + + /** @param ResolvesToExpression|array|bool|float|int|object|string|null $expressions */ + public function __construct(array|bool|float|int|null|object|string ...$expressions) + { + $this->expressions = $expressions; + } +} diff --git a/src/Builder/Aggregation/EqAggregation.php b/src/Builder/Aggregation/EqAggregation.php new file mode 100644 index 000000000..515bc375f --- /dev/null +++ b/src/Builder/Aggregation/EqAggregation.php @@ -0,0 +1,28 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } +} diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php new file mode 100644 index 000000000..63fd81e62 --- /dev/null +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -0,0 +1,35 @@ +input = $input; + $this->cond = $cond; + $this->as = $as; + $this->limit = $limit; + } +} diff --git a/src/Builder/Aggregation/GtAggregation.php b/src/Builder/Aggregation/GtAggregation.php new file mode 100644 index 000000000..8e8971dda --- /dev/null +++ b/src/Builder/Aggregation/GtAggregation.php @@ -0,0 +1,28 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } +} diff --git a/src/Builder/Aggregation/LtAggregation.php b/src/Builder/Aggregation/LtAggregation.php new file mode 100644 index 000000000..9e1804911 --- /dev/null +++ b/src/Builder/Aggregation/LtAggregation.php @@ -0,0 +1,28 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } +} diff --git a/src/Builder/Aggregation/NeAggregation.php b/src/Builder/Aggregation/NeAggregation.php new file mode 100644 index 000000000..77ee0c8cb --- /dev/null +++ b/src/Builder/Aggregation/NeAggregation.php @@ -0,0 +1,28 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } +} diff --git a/src/Builder/Query.php b/src/Builder/Query.php new file mode 100644 index 000000000..3c3613193 --- /dev/null +++ b/src/Builder/Query.php @@ -0,0 +1,38 @@ + $query */ + public array $query; + + /** @param ResolvesToQueryOperator|array|object $query */ + public function __construct(array|object ...$query) + { + $this->query = $query; + } +} diff --git a/src/Builder/Query/ExprQuery.php b/src/Builder/Query/ExprQuery.php new file mode 100644 index 000000000..b9ff3f4f0 --- /dev/null +++ b/src/Builder/Query/ExprQuery.php @@ -0,0 +1,20 @@ +expression = $expression; + } +} diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php new file mode 100644 index 000000000..6dc5064a9 --- /dev/null +++ b/src/Builder/Stage.php @@ -0,0 +1,46 @@ +limit = $limit; + } +} diff --git a/src/Builder/Stage/MatchStage.php b/src/Builder/Stage/MatchStage.php new file mode 100644 index 000000000..1ac91a812 --- /dev/null +++ b/src/Builder/Stage/MatchStage.php @@ -0,0 +1,21 @@ + $matchExpr */ + public array $matchExpr; + + /** @param ResolvesToMatchExpression|array|object $matchExpr */ + public function __construct(array|object ...$matchExpr) + { + $this->matchExpr = $matchExpr; + } +} diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php new file mode 100644 index 000000000..2979b99bd --- /dev/null +++ b/src/Builder/Stage/SortStage.php @@ -0,0 +1,20 @@ +sortSpecification = $sortSpecification; + } +} From 900c7485169bbbbffe906418e95e5a50345f28bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 26 Sep 2023 14:24:31 +0200 Subject: [PATCH 08/31] Add minimum for variadic arguments --- generator/config/pipeline-operators.yaml | 1 + generator/config/stages.yaml | 1 + generator/src/Definition/ArgumentDefinition.php | 1 + generator/src/ValueClassGenerator.php | 9 +++++++++ src/Builder/Aggregation/AndAggregation.php | 8 ++++++++ src/Builder/Stage/MatchStage.php | 8 ++++++++ 6 files changed, 28 insertions(+) diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index 43a01c731..96492bc2f 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -4,6 +4,7 @@ - name: expressions type: resolvesToExpression isVariadic: true + variadicMin: 1 - name: eq type: resolvesToBoolExpression args: diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index 7c438f567..54067cce9 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -3,6 +3,7 @@ - name: matchExpr type: resolvesToMatchExpression isVariadic: true + variadicMin: 1 - name: sort args: - name: sortSpecification diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index 2f6c4ab9b..9e0b813d1 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -10,6 +10,7 @@ public function __construct( public string $type, public bool $isOptional = false, public bool $isVariadic = false, + public ?int $variadicMin = null, ) { } } diff --git a/generator/src/ValueClassGenerator.php b/generator/src/ValueClassGenerator.php index 4412f6263..a86626bcb 100644 --- a/generator/src/ValueClassGenerator.php +++ b/generator/src/ValueClassGenerator.php @@ -45,6 +45,15 @@ public function createClassForObject(object $object): ClassType $constuctorParam->setType($nativeType); if ($argument->isVariadic) { $constuctor->setVariadic(); + + if ($argument->variadicMin !== null) { + $constuctor->addBody(<<name}) < {$argument->variadicMin}) { + throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', $argument->variadicMin, \count(\${$argument->name}))); + } + + PHP); + } } $constuctor->addComment('@param ' . $docType . ' $' . $argument->name); diff --git a/src/Builder/Aggregation/AndAggregation.php b/src/Builder/Aggregation/AndAggregation.php index a6bf95d62..8aae2df2d 100644 --- a/src/Builder/Aggregation/AndAggregation.php +++ b/src/Builder/Aggregation/AndAggregation.php @@ -6,9 +6,13 @@ namespace MongoDB\Builder\Aggregation; +use InvalidArgumentException; use MongoDB\Builder\Expression\ResolvesToBoolExpression; use MongoDB\Builder\Expression\ResolvesToExpression; +use function count; +use function sprintf; + class AndAggregation implements ResolvesToBoolExpression { /** @param list $expressions */ @@ -17,6 +21,10 @@ class AndAggregation implements ResolvesToBoolExpression /** @param ResolvesToExpression|array|bool|float|int|object|string|null $expressions */ public function __construct(array|bool|float|int|null|object|string ...$expressions) { + if (count($expressions) < 1) { + throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($expressions))); + } + $this->expressions = $expressions; } } diff --git a/src/Builder/Stage/MatchStage.php b/src/Builder/Stage/MatchStage.php index 1ac91a812..291c89e9a 100644 --- a/src/Builder/Stage/MatchStage.php +++ b/src/Builder/Stage/MatchStage.php @@ -6,8 +6,12 @@ namespace MongoDB\Builder\Stage; +use InvalidArgumentException; use MongoDB\Builder\Expression\ResolvesToMatchExpression; +use function count; +use function sprintf; + class MatchStage { /** @param list $matchExpr */ @@ -16,6 +20,10 @@ class MatchStage /** @param ResolvesToMatchExpression|array|object $matchExpr */ public function __construct(array|object ...$matchExpr) { + if (count($matchExpr) < 1) { + throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($matchExpr))); + } + $this->matchExpr = $matchExpr; } } From 1ad50f884bb6f26e4505366c3c8802f767249a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 27 Sep 2023 15:48:19 +0200 Subject: [PATCH 09/31] Add expression types --- .gitattributes | 10 ++ generator/README.md | 2 +- generator/bin/console | 2 +- generator/config/expressions.php | 79 +++++++++++ .../config/{config.php => operators.php} | 17 +-- generator/config/pipeline-operators.yaml | 38 +++--- generator/config/query-operators.yaml | 8 +- generator/config/stages.yaml | 6 +- generator/src/AbstractGenerator.php | 127 ++++++------------ generator/src/Command/GenerateCommand.php | 49 ++++--- .../src/Definition/ArgumentDefinition.php | 2 +- .../src/Definition/ExpressionDefinition.php | 17 +++ .../src/Definition/GeneratorDefinition.php | 11 +- generator/src/Definition/YamlReader.php | 8 +- generator/src/ExpressionClassGenerator.php | 79 +++++++++++ generator/src/FactoryClassGenerator.php | 71 ---------- generator/src/OperatorClassGenerator.php | 103 ++++++++++++++ generator/src/OperatorFactoryGenerator.php | 75 +++++++++++ generator/src/OperatorGenerator.php | 113 ++++++++++++++++ generator/src/ValueClassGenerator.php | 79 ----------- src/Builder/Aggregation.php | 107 +++++---------- src/Builder/Aggregation/AndAggregation.php | 11 +- src/Builder/Aggregation/EqAggregation.php | 19 +-- src/Builder/Aggregation/FilterAggregation.php | 43 ++++-- src/Builder/Aggregation/GtAggregation.php | 19 +-- src/Builder/Aggregation/LtAggregation.php | 19 +-- src/Builder/Aggregation/NeAggregation.php | 19 +-- src/Builder/Expression/Expression.php | 12 ++ src/Builder/Expression/ExpressionObject.php | 23 ++++ src/Builder/Expression/FieldPath.php | 19 +++ src/Builder/Expression/Literal.php | 19 +++ src/Builder/Expression/Operator.php | 15 +++ src/Builder/Expression/ResolvesToArray.php | 15 +++ .../Expression/ResolvesToArrayExpression.php | 8 -- src/Builder/Expression/ResolvesToBool.php | 12 ++ .../Expression/ResolvesToBoolExpression.php | 8 -- src/Builder/Expression/ResolvesToDate.php | 12 ++ src/Builder/Expression/ResolvesToDecimal.php | 15 +++ .../Expression/ResolvesToExpression.php | 8 -- src/Builder/Expression/ResolvesToFloat.php | 14 ++ src/Builder/Expression/ResolvesToInt.php | 14 ++ .../Expression/ResolvesToMatchExpression.php | 8 -- src/Builder/Expression/ResolvesToNull.php | 12 ++ src/Builder/Expression/ResolvesToNumber.php | 15 +++ .../Expression/ResolvesToNumberExpression.php | 8 -- src/Builder/Expression/ResolvesToObject.php | 15 +++ .../Expression/ResolvesToQueryOperator.php | 8 -- .../ResolvesToSortSpecification.php | 8 -- src/Builder/Expression/ResolvesToString.php | 12 ++ src/Builder/Expression/Variable.php | 19 +++ src/Builder/Query.php | 25 ++-- src/Builder/Query/AndQuery.php | 18 ++- src/Builder/Query/ExprQuery.php | 9 +- src/Builder/Stage.php | 37 +++-- src/Builder/Stage/LimitStage.php | 8 +- src/Builder/Stage/MatchStage.php | 7 +- src/Builder/Stage/SortStage.php | 6 +- 57 files changed, 990 insertions(+), 552 deletions(-) create mode 100644 generator/config/expressions.php rename generator/config/{config.php => operators.php} (74%) create mode 100644 generator/src/Definition/ExpressionDefinition.php create mode 100644 generator/src/ExpressionClassGenerator.php delete mode 100644 generator/src/FactoryClassGenerator.php create mode 100644 generator/src/OperatorClassGenerator.php create mode 100644 generator/src/OperatorFactoryGenerator.php create mode 100644 generator/src/OperatorGenerator.php delete mode 100644 generator/src/ValueClassGenerator.php create mode 100644 src/Builder/Expression/Expression.php create mode 100644 src/Builder/Expression/ExpressionObject.php create mode 100644 src/Builder/Expression/FieldPath.php create mode 100644 src/Builder/Expression/Literal.php create mode 100644 src/Builder/Expression/Operator.php create mode 100644 src/Builder/Expression/ResolvesToArray.php delete mode 100644 src/Builder/Expression/ResolvesToArrayExpression.php create mode 100644 src/Builder/Expression/ResolvesToBool.php delete mode 100644 src/Builder/Expression/ResolvesToBoolExpression.php create mode 100644 src/Builder/Expression/ResolvesToDate.php create mode 100644 src/Builder/Expression/ResolvesToDecimal.php delete mode 100644 src/Builder/Expression/ResolvesToExpression.php create mode 100644 src/Builder/Expression/ResolvesToFloat.php create mode 100644 src/Builder/Expression/ResolvesToInt.php delete mode 100644 src/Builder/Expression/ResolvesToMatchExpression.php create mode 100644 src/Builder/Expression/ResolvesToNull.php create mode 100644 src/Builder/Expression/ResolvesToNumber.php delete mode 100644 src/Builder/Expression/ResolvesToNumberExpression.php create mode 100644 src/Builder/Expression/ResolvesToObject.php delete mode 100644 src/Builder/Expression/ResolvesToQueryOperator.php delete mode 100644 src/Builder/Expression/ResolvesToSortSpecification.php create mode 100644 src/Builder/Expression/ResolvesToString.php create mode 100644 src/Builder/Expression/Variable.php diff --git a/.gitattributes b/.gitattributes index f55e8fa46..7cfb8f22f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,13 @@ 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/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 diff --git a/generator/README.md b/generator/README.md index 0b3a66abf..73bae9fad 100644 --- a/generator/README.md +++ b/generator/README.md @@ -5,7 +5,7 @@ The `generator` directory is not included in `mongodb/mongodb` package and is no ## Contributing -Updating the generated code can be done only by modifying the generator code, or it's configuration. +Updating the generated code can be done only by modifying the generator code, or its configuration. To run the generator, you need to have PHP 8.2+ installed and Composer. diff --git a/generator/bin/console b/generator/bin/console index 61902243c..0288aa279 100755 --- a/generator/bin/console +++ b/generator/bin/console @@ -12,5 +12,5 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')) { require __DIR__ . '/../vendor/autoload.php'; $application = new Application(); -$application->add(new GenerateCommand(__DIR__ . '/../config/config.php')); +$application->add(new GenerateCommand(__DIR__ . '/../../',__DIR__ . '/../config')); $application->run(); diff --git a/generator/config/expressions.php b/generator/config/expressions.php new file mode 100644 index 000000000..08a99f969 --- /dev/null +++ b/generator/config/expressions.php @@ -0,0 +1,79 @@ + [ + 'types' => ['mixed'], + ], + FieldPath::class => [ + 'class' => true, + 'implements' => [Expression::class], + 'types' => ['string'], + ], + Variable::class => [ + 'class' => true, + 'implements' => [Expression::class], + 'types' => ['string'], + ], + Literal::class => [ + 'class' => true, + 'implements' => [Expression::class], + 'types' => ['mixed'], + ], + ExpressionObject::class => [ + 'class' => true, + 'implements' => [Expression::class], + 'types' => ['array', stdClass::class, BSON\Document::class, BSON\Serializable::class], + ], + Operator::class => [ + 'implements' => [Expression::class], + 'types' => ['array', stdClass::class, BSON\Document::class, BSON\Serializable::class], + ], + ResolvesToArray::class => [ + 'implements' => [Expression::class], + 'types' => ['list', BSONArray::class, BSON\PackedArray::class], + ], + ResolvesToBool::class => [ + 'implements' => [Expression::class], + 'types' => ['bool'], + ], + ResolvesToDate::class => [ + 'implements' => [Expression::class], + 'types' => ['DateTimeInterface', 'UTCDateTime'], + ], + ResolvesToObject::class => [ + 'implements' => [Expression::class], + 'types' => ['array', 'object', BSON\Document::class, BSON\Serializable::class], + ], + ResolvesToNull::class => [ + 'implements' => [Expression::class], + 'types' => ['null'], + ], + ResolvesToNumber::class => [ + 'implements' => [Expression::class], + 'types' => ['int', 'float', BSON\Int64::class, BSON\Decimal128::class], + ], + ResolvesToDecimal::class => [ + 'implements' => [ResolvesToNumber::class], + 'types' => ['int', 'float', BSON\Int64::class, BSON\Decimal128::class], + ], + ResolvesToFloat::class => [ + 'implements' => [ResolvesToNumber::class], + 'types' => ['int', 'float', BSON\Int64::class], + ], + ResolvesToInt::class => [ + 'implements' => [ResolvesToNumber::class], + 'types' => ['int', BSON\Int64::class], + ], + ResolvesToString::class => [ + 'implements' => [Expression::class], + 'types' => ['string'], + ], +]; diff --git a/generator/config/config.php b/generator/config/operators.php similarity index 74% rename from generator/config/config.php rename to generator/config/operators.php index a0da4ade5..68709cb67 100644 --- a/generator/config/config.php +++ b/generator/config/operators.php @@ -2,8 +2,9 @@ namespace MongoDB\CodeGenerator\Config; -use MongoDB\CodeGenerator\FactoryClassGenerator; -use MongoDB\CodeGenerator\ValueClassGenerator; +use MongoDB\CodeGenerator\ExpressionClassGenerator; +use MongoDB\CodeGenerator\OperatorFactoryGenerator; +use MongoDB\CodeGenerator\OperatorClassGenerator; $src = __DIR__ . '/../../src'; $tests = __DIR__ . '/../../tests'; @@ -12,14 +13,14 @@ // Aggregation Pipeline Stages [ 'configFile' => __DIR__ . '/stages.yaml', - 'generatorClass' => ValueClassGenerator::class, + 'generatorClass' => OperatorClassGenerator::class, 'namespace' => 'MongoDB\\Builder\\Stage', 'filePath' => $src . '/Builder/Stage', 'classNameSuffix' => 'Stage', ], [ 'configFile' => __DIR__ . '/stages.yaml', - 'generatorClass' => FactoryClassGenerator::class, + 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Stage', 'filePath' => $src . '/Builder/Stage', 'classNameSuffix' => 'Stage', @@ -28,14 +29,14 @@ // Aggregation Pipeline Operators [ 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => ValueClassGenerator::class, + 'generatorClass' => OperatorClassGenerator::class, 'namespace' => 'MongoDB\\Builder\\Aggregation', 'filePath' => $src . '/Builder/Aggregation', 'classNameSuffix' => 'Aggregation', ], [ 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => FactoryClassGenerator::class, + 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Aggregation', 'filePath' => $src . '/Builder/Aggregation', 'classNameSuffix' => 'Aggregation', @@ -44,14 +45,14 @@ // Query Operators [ 'configFile' => __DIR__ . '/query-operators.yaml', - 'generatorClass' => ValueClassGenerator::class, + 'generatorClass' => OperatorClassGenerator::class, 'namespace' => 'MongoDB\\Builder\\Query', 'filePath' => $src . '/Builder/Query', 'classNameSuffix' => 'Query', ], [ 'configFile' => __DIR__ . '/query-operators.yaml', - 'generatorClass' => FactoryClassGenerator::class, + 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Query', 'filePath' => $src . '/Builder/Query', 'classNameSuffix' => 'Query', diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index 96492bc2f..633150927 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -1,49 +1,49 @@ - name: and - type: resolvesToBoolExpression + type: resolvesToBool args: - name: expressions - type: resolvesToExpression + type: expression isVariadic: true variadicMin: 1 - name: eq - type: resolvesToBoolExpression + type: resolvesToBool args: - name: expression1 - type: resolvesToExpression + type: expression - name: expression2 - type: resolvesToExpression + type: expression - name: gt - type: resolvesToBoolExpression + type: resolvesToBool args: - name: expression1 - type: resolvesToExpression + type: expression - name: expression2 - type: resolvesToExpression + type: expression - name: lt - type: resolvesToBoolExpression + type: resolvesToBool args: - name: expression1 - type: resolvesToExpression + type: expression - name: expression2 - type: resolvesToExpression + type: expression - name: ne - type: resolvesToBoolExpression + type: resolvesToBool args: - name: expression1 - type: resolvesToExpression + type: expression - name: expression2 - type: resolvesToExpression + type: expression - name: filter - type: resolvesToArrayExpression + type: resolvesToArray usesNamedArgs: true args: - name: input - type: resolvesToArrayExpression + type: resolvesToArray - name: cond - type: resolvesToBoolExpression + type: resolvesToBool - name: as - type: string + type: resolvesToString isOptional: true - name: limit - type: resolvesToNumberExpression + type: resolvesToInt isOptional: true diff --git a/generator/config/query-operators.yaml b/generator/config/query-operators.yaml index c217b7a77..a4a454756 100644 --- a/generator/config/query-operators.yaml +++ b/generator/config/query-operators.yaml @@ -1,11 +1,11 @@ - name: and - type: resolvesToBoolExpression + type: resolvesToBool args: - name: query - type: resolvesToQueryOperator + type: resolvesToBool isVariadic: true - name: expr - type: resolvesToExpression + type: expression args: - name: expression - type: resolvesToExpression + type: expression diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index 54067cce9..e303a2236 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -1,14 +1,14 @@ - name: match args: - name: matchExpr - type: resolvesToMatchExpression + type: expression isVariadic: true variadicMin: 1 - name: sort args: - name: sortSpecification - type: resolvesToSortSpecification + type: resolvesToObject - name: limit args: - name: limit - type: int + type: resolvesToInt diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index 2da0f70d9..f4e229660 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -4,124 +4,85 @@ namespace MongoDB\CodeGenerator; use InvalidArgumentException; -use MongoDB\Builder\Expression; -use MongoDB\CodeGenerator\Definition\ArgumentDefinition; -use MongoDB\CodeGenerator\Definition\GeneratorDefinition; -use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\PhpFile; +use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Printer; use Nette\PhpGenerator\PsrPrinter; +use function array_pop; +use function count; +use function current; use function dirname; +use function explode; use function file_put_contents; use function implode; -use function in_array; -use function interface_exists; use function is_dir; use function mkdir; -use function sort; -use function ucfirst; +use function sprintf; +use function str_replace; +use function str_starts_with; /** @internal */ abstract class AbstractGenerator { - /** @var array> */ - protected array $typeAliases = [ - 'resolvesToExpression' => [Expression\ResolvesToExpression::class, 'array', 'object', 'string', 'int', 'float', 'bool', 'null'], - 'resolvesToArrayExpression' => [Expression\ResolvesToArrayExpression::class, 'array', 'object', 'string'], - 'resolvesToBoolExpression' => [Expression\ResolvesToBoolExpression::class, 'array', 'object', 'string', 'bool'], - 'resolvesToMatchExpression' => ['array', 'object', Expression\ResolvesToMatchExpression::class], - 'resolvesToNumberExpression' => [Expression\ResolvesToBoolExpression::class, 'array', 'object', 'string', 'int', 'float'], - 'resolvesToQueryOperator' => ['array', 'object', Expression\ResolvesToQueryOperator::class], - 'resolvesToSortSpecification' => ['array', 'object', Expression\ResolvesToSortSpecification::class], - ]; - - protected GeneratorDefinition $definition; protected Printer $printer; - public function __construct(GeneratorDefinition $definition) - { - $this->validate($definition); - - $this->definition = $definition; + public function __construct( + private string $rootDir + ) { $this->printer = new PsrPrinter(); } - /** @throws InvalidArgumentException when definition is invalid */ - protected function validate(GeneratorDefinition $definition): void + final protected function splitNamespaceAndClassName(string $fqcn): array { - } + $parts = explode('\\', $fqcn); + $className = array_pop($parts); - public function createClassesForObjects(array $objects): void - { - foreach ($objects as $object) { - $this->createFileForClass( - $this->definition->filePath, - $this->createClassForObject($object), - ); - } + return [implode('\\', $parts), $className]; } - abstract public function createClassForObject(object $object): ClassType; - - /** @return array{native:string,doc:string} */ - final protected function generateTypeString(ArgumentDefinition $arg): array + protected function writeFile(PhpNamespace $namespace): void { - $type = $arg->type; - $nativeTypes = $this->typeAliases[$type] ?? [$type]; - $docTypes = $nativeTypes; - - foreach ($nativeTypes as $key => $typeName) { - if (interface_exists($typeName)) { - $nativeTypes[$key] = $docTypes[$key] = '\\' . $typeName; - // A union cannot contain both object and a class type, which is redundant and causes a PHP error - // @todo replace "object" with "stdClass" and force any class object to implement the proper interface - if (in_array('object', $nativeTypes, true)) { - unset($nativeTypes[$key]); - } - } + $classes = $namespace->getClasses(); + if (count($classes) !== 1) { + throw new InvalidArgumentException(sprintf('Expected exactly one class in namespace "%s", got %d.', $namespace->getName(), count($classes))); } - sort($nativeTypes); - sort($docTypes); + $filename = $this->rootDir . '/' . $this->getFileName($namespace->getName(), current($classes)->getName()); - if ($arg->isOptional) { - $nativeTypes[] = 'null'; - $docTypes[] = 'null'; + $dirname = dirname($filename); + if (! is_dir($dirname)) { + mkdir($dirname, 0775, true); } - return [ - 'native' => implode('|', $nativeTypes), - 'doc' => implode('|', $docTypes), - ]; - } - - protected function getClassName(object $object): string - { - return ucfirst($object->name) . $this->definition->classNameSuffix; - } - - protected function createFileForClass(string $dirname, ClassType $class, ?string $namespace = null): void - { - $fullName = $dirname . '/' . $class->getName() . '.php'; - $file = new PhpFile(); - $namespace = $file->addNamespace($namespace ?? $this->definition->namespace); - $namespace->add($class); + $file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); + $file->addNamespace($namespace); - $this->writeFileFromGenerator($fullName, $file); + file_put_contents($filename, $this->printer->printFile($file)); } - protected function writeFileFromGenerator(string $filename, PhpFile $file): void + /** + * Thanks to PSR-4, the file name can be determined from the fully qualified class name. + * + * @param string ...$fqcn Fully qualified class name, merged if multiple parts + * @return string File name relative to the root directory + */ + private function getFileName(string ...$fqcn): string { - $dirname = dirname($filename); + $fqcn = implode('\\', $fqcn); - $file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); - - if (! is_dir($dirname)) { - mkdir($dirname, 0775, true); + // Config from composer.json + $config = [ + 'MongoDB\\Tests\\' => 'tests/', + 'MongoDB\\' => 'src/', + ]; + foreach ($config as $namespace => $directory) { + if (str_starts_with($fqcn, $namespace)) { + return $directory . str_replace([$namespace, '\\'], ['', '/'], $fqcn) . '.php'; + } } - file_put_contents($filename, $this->printer->printFile($file)); + throw new InvalidArgumentException(sprintf('Could not determine file name for "%s"', $fqcn)); } } diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index 5eb1f7696..2d0d29e5e 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -3,8 +3,10 @@ namespace MongoDB\CodeGenerator\Command; +use MongoDB\CodeGenerator\Definition\ExpressionDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; -use MongoDB\CodeGenerator\Definition\YamlReader; +use MongoDB\CodeGenerator\ExpressionClassGenerator; +use MongoDB\CodeGenerator\OperatorGenerator; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -19,40 +21,50 @@ #[AsCommand(name: 'generate', description: 'Generate code for mongodb/mongodb library')] final class GenerateCommand extends Command { - private YamlReader $yamlReader; - public function __construct( - private string $configFile, + private string $rootDir, + private string $configDir, ) { parent::__construct(); - - $this->yamlReader = new YamlReader(); - } - - public function configure(): void - { - $this->addOption('force', 'f', null, 'Force generation of all files'); } public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Generating code for mongodb/mongodb library'); - $config = require $this->configFile; + $this->generateExpressionClasses($output); + + $config = require $this->configDir . '/operators.php'; assert(is_array($config)); - // @todo This is a hack to get the first pipeline operator config foreach ($config as $key => $def) { assert(is_array($def)); - $this->generate(new GeneratorDefinition(...$def), $output); + $this->generate($def, $output); } return Command::SUCCESS; } - private function generate(GeneratorDefinition $definition, OutputInterface $output): void + private function generateExpressionClasses(OutputInterface $output): void { - $output->writeln(sprintf('Generating code for %s with %s', basename($definition->configFile), $definition->generatorClass)); + $output->writeln('Generating expression classes'); + + $config = require $this->configDir . '/expressions.php'; + assert(is_array($config)); + + $generator = new ExpressionClassGenerator($this->rootDir); + foreach ($config as $name => $def) { + assert(is_array($def)); + $def = new ExpressionDefinition($name, ...$def); + $generator->generate($def); + } + } + + private function generate(array $def, OutputInterface $output): void + { + $definition = new GeneratorDefinition(...$def); + + $output->writeln(sprintf('Generating classes for %s with %s', basename($definition->configFile), $definition->generatorClass)); if (! class_exists($definition->generatorClass)) { $output->writeln(sprintf('Generator class %s does not exist', $definition->generatorClass)); @@ -61,7 +73,8 @@ private function generate(GeneratorDefinition $definition, OutputInterface $outp } $generatorClass = $definition->generatorClass; - $generator = new $generatorClass($definition); - $generator->createClassesForObjects($this->yamlReader->read($definition->configFile)); + $generator = new $generatorClass($this->rootDir); + assert($generator instanceof OperatorGenerator); + $generator->generate($definition); } } diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index 9e0b813d1..2d120ee83 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -10,7 +10,7 @@ public function __construct( public string $type, public bool $isOptional = false, public bool $isVariadic = false, - public ?int $variadicMin = null, + public int $variadicMin = 1, ) { } } diff --git a/generator/src/Definition/ExpressionDefinition.php b/generator/src/Definition/ExpressionDefinition.php new file mode 100644 index 000000000..c629919ec --- /dev/null +++ b/generator/src/Definition/ExpressionDefinition.php @@ -0,0 +1,17 @@ + */ + public array $types, + public bool $class = false, + /** @var list */ + public array $implements = [], + ) { + } +} diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index 94bba5bbe..6b0ce038a 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -4,7 +4,7 @@ namespace MongoDB\CodeGenerator\Definition; use InvalidArgumentException; -use MongoDB\CodeGenerator\AbstractGenerator; +use MongoDB\CodeGenerator\OperatorGenerator; use function is_subclass_of; use function sprintf; @@ -15,7 +15,10 @@ { public function __construct( public readonly string $configFile, - /** @var class-string */ + /** + * @var class-string + * @psalm-assert class-string + */ public readonly string $generatorClass, public readonly string $namespace, public readonly string $filePath, @@ -35,8 +38,8 @@ public function __construct( throw new InvalidArgumentException(sprintf('Namespace must not end with "\\". Got "%s".', $this->namespace)); } - if (! is_subclass_of($this->generatorClass, AbstractGenerator::class)) { - throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $this->generatorClass, AbstractGenerator::class)); + if (! is_subclass_of($this->generatorClass, OperatorGenerator::class)) { + throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $this->generatorClass, OperatorGenerator::class)); } } } diff --git a/generator/src/Definition/YamlReader.php b/generator/src/Definition/YamlReader.php index 5aae05697..cbfa42584 100644 --- a/generator/src/Definition/YamlReader.php +++ b/generator/src/Definition/YamlReader.php @@ -12,13 +12,13 @@ final class YamlReader { /** @var array> */ - private array $definitions = []; + private static array $definitions = []; /** @return list */ public function read(string $filename): array { - if (array_key_exists($filename, $this->definitions)) { - return $this->definitions[$filename]; + if (array_key_exists($filename, self::$definitions)) { + return self::$definitions[$filename]; } $config = Yaml::parseFile($filename); @@ -30,6 +30,6 @@ public function read(string $filename): array $definitions[] = new OperatorDefinition(...$operator); } - return $definitions; + return self::$definitions[$filename] = $definitions; } } diff --git a/generator/src/ExpressionClassGenerator.php b/generator/src/ExpressionClassGenerator.php new file mode 100644 index 000000000..f8c806f98 --- /dev/null +++ b/generator/src/ExpressionClassGenerator.php @@ -0,0 +1,79 @@ +writeFile($this->createClassOrInterface($definition)); + } + + /** + * @param string $className + * @return object{$class:ClassType|InterfaceType, $use: + */ + public function createClassOrInterface(ExpressionDefinition $definition): PhpNamespace + { + [$namespace, $className] = $this->splitNamespaceAndClassName($definition->name); + $namespace = new PhpNamespace($namespace); + foreach ($definition->implements as $interface) { + $namespace->addUse($interface); + } + + $types = array_map( + fn (string $type): string => match ($type) { + 'list' => 'array', + default => $type, + }, + $definition->types, + ); + + if ($definition->class) { + $class = $namespace->addClass($className); + $class->setImplements($definition->implements); + $class->setFinal(); + + // Replace with promoted property in PHP 8 + $propertyType = Type::union(...$types); + $class->addProperty('expression') + ->setType($propertyType) + ->setPublic(); + + $constructor = $class->addMethod('__construct'); + $constructor->addParameter('expression')->setType($propertyType); + $constructor->addBody('$this->expression = $expression;'); + } else { + $class = $namespace->addInterface($className); + $class->setExtends($definition->implements); + } + + $types = array_map( + function (string $type): string|Literal { + if (str_contains($type, '\\')) { + return new Literal(sprintf('\\%s::class', $type)); + } + + return $type; + }, + $definition->types, + ); + + $class->addConstant('ACCEPTED_TYPES', $types); + + return $namespace; + } +} diff --git a/generator/src/FactoryClassGenerator.php b/generator/src/FactoryClassGenerator.php deleted file mode 100644 index cfa4a3a08..000000000 --- a/generator/src/FactoryClassGenerator.php +++ /dev/null @@ -1,71 +0,0 @@ -definition->namespace); - $className = array_pop($namespaceParts); - $namespace = implode('\\', $namespaceParts); - - $this->createFileForClass( - dirname($this->definition->filePath), - $this->createFactoryClass($objects, $className), - $namespace, - ); - } - - private function createFactoryClass(array $objects, string $className): ClassType - { - $class = new ClassType($className); - $class->setFinal(); - $class->addMethod('__construct')->setPrivate() - ->setComment('This class cannot be instantiated.'); - - foreach ($objects as $object) { - assert($object instanceof OperatorDefinition); - $operatorClassName = '\\' . $this->definition->namespace . '\\' . $this->getClassName($object); - - $method = $class->addMethod($object->name); - $method->setStatic(); - $method->addBody('return new ' . $operatorClassName . '('); - foreach ($object->arguments as $argument) { - ['native' => $nativeType, 'doc' => $docType] = $this->generateTypeString($argument); - - $parameter = $method->addParameter($argument->name); - $parameter->setType($nativeType); - if ($argument->isVariadic) { - $method->setVariadic(); - } - - $method->addComment('@param ' . $docType . ' $' . $argument->name); - $method->addBody(' $' . $argument->name . ','); - } - - $method->addBody(');'); - $method->addComment('@return ' . $operatorClassName); - $method->setReturnType($operatorClassName); - } - - return $class; - } - - public function createClassForObject(object $object): ClassType - { - // Not used - } -} diff --git a/generator/src/OperatorClassGenerator.php b/generator/src/OperatorClassGenerator.php new file mode 100644 index 000000000..3615959c5 --- /dev/null +++ b/generator/src/OperatorClassGenerator.php @@ -0,0 +1,103 @@ +getOperators($definition) as $operator) { + $this->writeFile($this->createClass($definition, $operator)); + } + } + + public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator): PhpNamespace + { + $namespace = new PhpNamespace($definition->namespace); + $class = $namespace->addClass($this->getOperatorClassName($definition, $operator)); + $class->setImplements($this->getInterfaces($operator)); + + $constuctor = $class->addMethod('__construct'); + foreach ($operator->arguments as $argument) { + $type = $this->generateExpressionTypes($argument); + foreach ($type->use as $use) { + $namespace->addUse($use); + } + + // Property + $propertyComment = ''; + $property = $class->addProperty($argument->name); + if ($argument->isVariadic) { + $property->setType('array'); + $propertyComment .= '@param list<' . $type->doc . '> ...$' . $argument->name; + } else { + $property->setType($type->native); + } + + $property->setComment($propertyComment); + + // Constructor + $constuctorParam = $constuctor->addParameter($argument->name); + $constuctorParam->setType($type->native); + if ($argument->isVariadic) { + $constuctor->setVariadic(); + + if ($argument->variadicMin > 0) { + $constuctor->addBody(<<name}) < {$argument->variadicMin}) { + throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', $argument->variadicMin, \count(\${$argument->name}))); + } + + PHP); + } + } elseif ($argument->isOptional) { + $constuctorParam->setDefaultValue(null); + } + + $constuctor->addComment('@param ' . $type->doc . ' $' . $argument->name); + + // List type must be validated with array_is_list() + if ($type->list) { + $constuctor->addBody(<<name}) && ! \array_is_list(\${$argument->name})) { + throw new \InvalidArgumentException(\sprintf('Expected \${$argument->name} argument to be a list, got an associative array.')); + } + PHP); + } + + // Set property from constructor argument + $constuctor->addBody('$this->' . $argument->name . ' = $' . $argument->name . ';'); + } + + return $namespace; + } + + /** + * Operator classes interfaces are defined by their return type as a MongoDB expression. + */ + private function getInterfaces(OperatorDefinition $definition): array + { + if ($definition->type === null) { + return []; + } + + $interface = $this->getExpressionTypeInterface($definition->type); + if (! interface_exists($interface)) { + throw new RuntimeException(sprintf('"%s" is not an interface.', $interface)); + } + + return [$interface]; + } +} diff --git a/generator/src/OperatorFactoryGenerator.php b/generator/src/OperatorFactoryGenerator.php new file mode 100644 index 000000000..1c989ac80 --- /dev/null +++ b/generator/src/OperatorFactoryGenerator.php @@ -0,0 +1,75 @@ +writeFile($this->createFactoryClass($definition)); + } + + private function createFactoryClass(GeneratorDefinition $definition): PhpNamespace + { + // Use the operators namespace as factory class name. + [$namespace, $className] = $this->splitNamespaceAndClassName($definition->namespace); + $namespace = new PhpNamespace($namespace); + $class = $namespace->addClass($className); + $class->setFinal(); + + // Pedantry requires methods to be ordered alphabetically + $operators = $this->getOperators($definition); + usort($operators, fn (OperatorDefinition $a, OperatorDefinition $b) => $a->name <=> $b->name); + + foreach ($operators as $operator) { + $operatorClassName = '\\' . $definition->namespace . '\\' . $this->getOperatorClassName($definition, $operator); + $namespace->addUse($operatorClassName); + + $method = $class->addMethod($operator->name); + $method->setStatic(); + $args = []; + foreach ($operator->arguments as $argument) { + $type = $this->generateExpressionTypes($argument); + foreach ($type->use as $use) { + $namespace->addUse($use); + } + + $parameter = $method->addParameter($argument->name); + $parameter->setType($type->native); + if ($argument->isVariadic) { + $method->setVariadic(); + $method->addComment('@param ' . $type->doc . ' ...$' . $argument->name); + $args[] = '...$' . $argument->name; + } else { + if ($argument->isOptional) { + $parameter->setDefaultValue(null); + } + + $method->addComment('@param ' . $type->doc . ' $' . $argument->name); + $args[] = '$' . $argument->name; + } + } + + $operatorShortClassName = ltrim(str_replace($definition->namespace, '', $operatorClassName), '\\'); + $method->addBody('return new ' . $operatorShortClassName . '(' . implode(', ', $args) . ');'); + $method->setReturnType($operatorClassName); + } + + // Pedantry requires private methods to be at the end + $class->addMethod('__construct')->setPrivate() + ->setComment('This class cannot be instantiated.'); + + return $namespace; + } +} diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php new file mode 100644 index 000000000..d0eb56de2 --- /dev/null +++ b/generator/src/OperatorGenerator.php @@ -0,0 +1,113 @@ +yamlReader = new YamlReader(); + } + + abstract public function generate(GeneratorDefinition $definition): void; + + /** @return list */ + final protected function getOperators(GeneratorDefinition $definition): array + { + return $this->yamlReader->read($definition->configFile); + } + + final protected function getOperatorClassName(GeneratorDefinition $definition, OperatorDefinition $operator): string + { + return ucfirst($operator->name) . $definition->classNameSuffix; + } + + /** @return class-string */ + final protected function getExpressionTypeInterface(string $type): string + { + $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($type); + + if (! interface_exists($interface) || ! is_subclass_of($interface, Expression::class) && $interface !== Expression::class) { + throw new InvalidArgumentException(sprintf('Invalid expression type "%s".', $type)); + } + + return $interface; + } + + /** + * Expression types can contain class names, interface, native types or "list" + * + * @return object{native:string,doc:string,use:list,list:bool} + */ + final protected function generateExpressionTypes(ArgumentDefinition $arg): object + { + $interface = $this->getExpressionTypeInterface($arg->type); + $docTypes = $nativeTypes = array_merge([$interface], $interface::ACCEPTED_TYPES); + $listCheck = false; + $use = []; + + foreach ($nativeTypes as $key => $typeName) { + if ($typeName === 'list') { + $listCheck = true; + $nativeTypes[$key] = 'array'; + $docTypes[$key] = 'list'; + $use[] = '\\' . Expression::class; + continue; + } + + if (interface_exists($typeName) || class_exists($typeName)) { + $use[] = $nativeTypes[$key] = '\\' . $typeName; + //$nativeTypes[$key] = $docTypes[$key] = '\\' . $typeName; + $docTypes[$key] = $this->splitNamespaceAndClassName($typeName)[1]; + // A union cannot contain both object and a class type + if (in_array('object', $nativeTypes, true)) { + unset($nativeTypes[$key]); + } + } + } + + if ($arg->isOptional) { + $nativeTypes[] = 'null'; + $docTypes[] = 'null'; + } + + // mixed can only be used as a standalone type + if (in_array('mixed', $nativeTypes, true)) { + $nativeTypes = ['mixed']; + } + + sort($nativeTypes); + sort($docTypes); + sort($use); + + return (object) [ + 'native' => Type::union(...array_unique($nativeTypes)), + 'doc' => Type::union(...array_unique($docTypes)), + 'use' => array_unique($use), + 'list' => $listCheck, + ]; + } +} diff --git a/generator/src/ValueClassGenerator.php b/generator/src/ValueClassGenerator.php deleted file mode 100644 index a86626bcb..000000000 --- a/generator/src/ValueClassGenerator.php +++ /dev/null @@ -1,79 +0,0 @@ -getClassName($object)); - $class->setImplements($this->getInterfaces($object)); - - $constuctor = $class->addMethod('__construct'); - - foreach ($object->arguments as $argument) { - ['native' => $nativeType, 'doc' => $docType] = $this->generateTypeString($argument); - - // Property - $propertyComment = ''; - $property = $class->addProperty($argument->name); - if ($argument->isVariadic) { - $property->setType('array'); - $propertyComment .= '@param list<' . $docType . '> $' . $argument->name; - } else { - $property->setType($nativeType); - } - - $property->setComment($propertyComment); - - // Constructor - $constuctorParam = $constuctor->addParameter($argument->name); - $constuctorParam->setType($nativeType); - if ($argument->isVariadic) { - $constuctor->setVariadic(); - - if ($argument->variadicMin !== null) { - $constuctor->addBody(<<name}) < {$argument->variadicMin}) { - throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', $argument->variadicMin, \count(\${$argument->name}))); - } - - PHP); - } - } - - $constuctor->addComment('@param ' . $docType . ' $' . $argument->name); - $constuctor->addBody('$this->' . $argument->name . ' = $' . $argument->name . ';'); - } - - return $class; - } - - private function getInterfaces(OperatorDefinition $definition): array - { - if ($definition->type === null) { - return []; - } - - $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($definition->type); - if (! interface_exists($interface)) { - throw new RuntimeException('Interface ' . $interface . ' does not exist'); - } - - return [$interface]; - } -} diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php index 4226e5a3d..5ab622417 100644 --- a/src/Builder/Aggregation.php +++ b/src/Builder/Aggregation.php @@ -6,105 +6,66 @@ namespace MongoDB\Builder; +use MongoDB\BSON\Int64; +use MongoDB\BSON\PackedArray; use MongoDB\Builder\Aggregation\AndAggregation; use MongoDB\Builder\Aggregation\EqAggregation; use MongoDB\Builder\Aggregation\FilterAggregation; use MongoDB\Builder\Aggregation\GtAggregation; use MongoDB\Builder\Aggregation\LtAggregation; use MongoDB\Builder\Aggregation\NeAggregation; -use MongoDB\Builder\Expression\ResolvesToArrayExpression; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; -use MongoDB\Builder\Expression\ResolvesToExpression; +use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ResolvesToArray; +use MongoDB\Builder\Expression\ResolvesToBool; +use MongoDB\Builder\Expression\ResolvesToInt; +use MongoDB\Builder\Expression\ResolvesToString; +use MongoDB\Model\BSONArray; final class Aggregation { - /** - * This class cannot be instantiated. - */ - private function __construct() + public static function and(mixed ...$expressions): AndAggregation { + return new AndAggregation(...$expressions); } - /** @param ResolvesToExpression|array|bool|float|int|object|string|null $expressions */ - public static function and(array|bool|float|int|null|object|string ...$expressions): Aggregation\AndAggregation + public static function eq(mixed $expression1, mixed $expression2): EqAggregation { - return new AndAggregation( - $expressions, - ); + return new EqAggregation($expression1, $expression2); } /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 + * @param BSONArray|PackedArray|ResolvesToArray|list $input + * @param ResolvesToString|string|null $as + * @param Int64|ResolvesToInt|int|null $limit */ - public static function eq( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ): Aggregation\EqAggregation { - return new EqAggregation( - $expression1, - $expression2, - ); + public static function filter( + PackedArray|ResolvesToArray|BSONArray|array $input, + ResolvesToBool|bool $cond, + ResolvesToString|null|string $as = null, + Int64|ResolvesToInt|int|null $limit = null, + ): FilterAggregation { + return new FilterAggregation($input, $cond, $as, $limit); } - /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 - */ - public static function gt( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ): Aggregation\GtAggregation { - return new GtAggregation( - $expression1, - $expression2, - ); + public static function gt(mixed $expression1, mixed $expression2): GtAggregation + { + return new GtAggregation($expression1, $expression2); } - /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 - */ - public static function lt( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ): Aggregation\LtAggregation { - return new LtAggregation( - $expression1, - $expression2, - ); + public static function lt(mixed $expression1, mixed $expression2): LtAggregation + { + return new LtAggregation($expression1, $expression2); } - /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 - */ - public static function ne( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ): Aggregation\NeAggregation { - return new NeAggregation( - $expression1, - $expression2, - ); + public static function ne(mixed $expression1, mixed $expression2): NeAggregation + { + return new NeAggregation($expression1, $expression2); } /** - * @param ResolvesToArrayExpression|array|object|string $input - * @param ResolvesToBoolExpression|array|bool|object|string $cond - * @param ResolvesToBoolExpression|array|float|int|object|string|null $limit + * This class cannot be instantiated. */ - public static function filter( - array|object|string $input, - array|bool|object|string $cond, - string|null $as, - array|float|int|object|string|null $limit, - ): Aggregation\FilterAggregation { - return new FilterAggregation( - $input, - $cond, - $as, - $limit, - ); + private function __construct() + { } } diff --git a/src/Builder/Aggregation/AndAggregation.php b/src/Builder/Aggregation/AndAggregation.php index 8aae2df2d..55e20e223 100644 --- a/src/Builder/Aggregation/AndAggregation.php +++ b/src/Builder/Aggregation/AndAggregation.php @@ -7,19 +7,18 @@ namespace MongoDB\Builder\Aggregation; use InvalidArgumentException; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; -use MongoDB\Builder\Expression\ResolvesToExpression; +use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ResolvesToBool; use function count; use function sprintf; -class AndAggregation implements ResolvesToBoolExpression +class AndAggregation implements ResolvesToBool { - /** @param list $expressions */ + /** @param list ...$expressions */ public array $expressions; - /** @param ResolvesToExpression|array|bool|float|int|object|string|null $expressions */ - public function __construct(array|bool|float|int|null|object|string ...$expressions) + public function __construct(mixed ...$expressions) { if (count($expressions) < 1) { throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($expressions))); diff --git a/src/Builder/Aggregation/EqAggregation.php b/src/Builder/Aggregation/EqAggregation.php index 515bc375f..92f487e2c 100644 --- a/src/Builder/Aggregation/EqAggregation.php +++ b/src/Builder/Aggregation/EqAggregation.php @@ -6,22 +6,15 @@ namespace MongoDB\Builder\Aggregation; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; -use MongoDB\Builder\Expression\ResolvesToExpression; +use MongoDB\Builder\Expression\ResolvesToBool; -class EqAggregation implements ResolvesToBoolExpression +class EqAggregation implements ResolvesToBool { - public array|bool|float|int|null|object|string $expression1; - public array|bool|float|int|null|object|string $expression2; + public mixed $expression1; + public mixed $expression2; - /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 - */ - public function __construct( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ) { + public function __construct(mixed $expression1, mixed $expression2) + { $this->expression1 = $expression1; $this->expression2 = $expression2; } diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php index 63fd81e62..9ca99b0f0 100644 --- a/src/Builder/Aggregation/FilterAggregation.php +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -6,27 +6,42 @@ namespace MongoDB\Builder\Aggregation; -use MongoDB\Builder\Expression\ResolvesToArrayExpression; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; +use InvalidArgumentException; +use MongoDB\BSON\Int64; +use MongoDB\BSON\PackedArray; +use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ResolvesToArray; +use MongoDB\Builder\Expression\ResolvesToBool; +use MongoDB\Builder\Expression\ResolvesToInt; +use MongoDB\Builder\Expression\ResolvesToString; +use MongoDB\Model\BSONArray; -class FilterAggregation implements ResolvesToArrayExpression +use function array_is_list; +use function is_array; +use function sprintf; + +class FilterAggregation implements ResolvesToArray { - public array|object|string $input; - public array|bool|object|string $cond; - public string|null $as; - public array|float|int|object|string|null $limit; + public PackedArray|ResolvesToArray|BSONArray|array $input; + public ResolvesToBool|bool $cond; + public ResolvesToString|null|string $as; + public Int64|ResolvesToInt|int|null $limit; /** - * @param ResolvesToArrayExpression|array|object|string $input - * @param ResolvesToBoolExpression|array|bool|object|string $cond - * @param ResolvesToBoolExpression|array|float|int|object|string|null $limit + * @param BSONArray|PackedArray|ResolvesToArray|list $input + * @param ResolvesToString|string|null $as + * @param Int64|ResolvesToInt|int|null $limit */ public function __construct( - array|object|string $input, - array|bool|object|string $cond, - string|null $as, - array|float|int|object|string|null $limit, + PackedArray|ResolvesToArray|BSONArray|array $input, + ResolvesToBool|bool $cond, + ResolvesToString|null|string $as = null, + Int64|ResolvesToInt|int|null $limit = null, ) { + if (is_array($input) && ! array_is_list($input)) { + throw new InvalidArgumentException(sprintf('Expected $input argument to be a list, got an associative array.')); + } + $this->input = $input; $this->cond = $cond; $this->as = $as; diff --git a/src/Builder/Aggregation/GtAggregation.php b/src/Builder/Aggregation/GtAggregation.php index 8e8971dda..0817e3496 100644 --- a/src/Builder/Aggregation/GtAggregation.php +++ b/src/Builder/Aggregation/GtAggregation.php @@ -6,22 +6,15 @@ namespace MongoDB\Builder\Aggregation; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; -use MongoDB\Builder\Expression\ResolvesToExpression; +use MongoDB\Builder\Expression\ResolvesToBool; -class GtAggregation implements ResolvesToBoolExpression +class GtAggregation implements ResolvesToBool { - public array|bool|float|int|null|object|string $expression1; - public array|bool|float|int|null|object|string $expression2; + public mixed $expression1; + public mixed $expression2; - /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 - */ - public function __construct( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ) { + public function __construct(mixed $expression1, mixed $expression2) + { $this->expression1 = $expression1; $this->expression2 = $expression2; } diff --git a/src/Builder/Aggregation/LtAggregation.php b/src/Builder/Aggregation/LtAggregation.php index 9e1804911..d2271e68d 100644 --- a/src/Builder/Aggregation/LtAggregation.php +++ b/src/Builder/Aggregation/LtAggregation.php @@ -6,22 +6,15 @@ namespace MongoDB\Builder\Aggregation; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; -use MongoDB\Builder\Expression\ResolvesToExpression; +use MongoDB\Builder\Expression\ResolvesToBool; -class LtAggregation implements ResolvesToBoolExpression +class LtAggregation implements ResolvesToBool { - public array|bool|float|int|null|object|string $expression1; - public array|bool|float|int|null|object|string $expression2; + public mixed $expression1; + public mixed $expression2; - /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 - */ - public function __construct( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ) { + public function __construct(mixed $expression1, mixed $expression2) + { $this->expression1 = $expression1; $this->expression2 = $expression2; } diff --git a/src/Builder/Aggregation/NeAggregation.php b/src/Builder/Aggregation/NeAggregation.php index 77ee0c8cb..849885c70 100644 --- a/src/Builder/Aggregation/NeAggregation.php +++ b/src/Builder/Aggregation/NeAggregation.php @@ -6,22 +6,15 @@ namespace MongoDB\Builder\Aggregation; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; -use MongoDB\Builder\Expression\ResolvesToExpression; +use MongoDB\Builder\Expression\ResolvesToBool; -class NeAggregation implements ResolvesToBoolExpression +class NeAggregation implements ResolvesToBool { - public array|bool|float|int|null|object|string $expression1; - public array|bool|float|int|null|object|string $expression2; + public mixed $expression1; + public mixed $expression2; - /** - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression1 - * @param ResolvesToExpression|array|bool|float|int|object|string|null $expression2 - */ - public function __construct( - array|bool|float|int|null|object|string $expression1, - array|bool|float|int|null|object|string $expression2, - ) { + public function __construct(mixed $expression1, mixed $expression2) + { $this->expression1 = $expression1; $this->expression2 = $expression2; } diff --git a/src/Builder/Expression/Expression.php b/src/Builder/Expression/Expression.php new file mode 100644 index 000000000..665e6cb50 --- /dev/null +++ b/src/Builder/Expression/Expression.php @@ -0,0 +1,12 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/FieldPath.php b/src/Builder/Expression/FieldPath.php new file mode 100644 index 000000000..0b55db444 --- /dev/null +++ b/src/Builder/Expression/FieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/Literal.php b/src/Builder/Expression/Literal.php new file mode 100644 index 000000000..ac35f322a --- /dev/null +++ b/src/Builder/Expression/Literal.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/Operator.php b/src/Builder/Expression/Operator.php new file mode 100644 index 000000000..4f8eb08bb --- /dev/null +++ b/src/Builder/Expression/Operator.php @@ -0,0 +1,15 @@ +expression = $expression; + } +} diff --git a/src/Builder/Query.php b/src/Builder/Query.php index 3c3613193..50b8410bd 100644 --- a/src/Builder/Query.php +++ b/src/Builder/Query.php @@ -6,33 +6,26 @@ namespace MongoDB\Builder; -use MongoDB\Builder\Expression\ResolvesToExpression; -use MongoDB\Builder\Expression\ResolvesToQueryOperator; +use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Query\AndQuery; use MongoDB\Builder\Query\ExprQuery; final class Query { - /** - * This class cannot be instantiated. - */ - private function __construct() + public static function and(ResolvesToBool|bool ...$query): AndQuery { + return new AndQuery(...$query); } - /** @param ResolvesToQueryOperator|array|object $query */ - public static function and(array|object ...$query): Query\AndQuery + public static function expr(mixed $expression): ExprQuery { - return new AndQuery( - $query, - ); + return new ExprQuery($expression); } - /** @param ResolvesToExpression|array|bool|float|int|object|string|null $expression */ - public static function expr(array|bool|float|int|null|object|string $expression): Query\ExprQuery + /** + * This class cannot be instantiated. + */ + private function __construct() { - return new ExprQuery( - $expression, - ); } } diff --git a/src/Builder/Query/AndQuery.php b/src/Builder/Query/AndQuery.php index be226e63b..eed3372e5 100644 --- a/src/Builder/Query/AndQuery.php +++ b/src/Builder/Query/AndQuery.php @@ -6,17 +6,23 @@ namespace MongoDB\Builder\Query; -use MongoDB\Builder\Expression\ResolvesToBoolExpression; -use MongoDB\Builder\Expression\ResolvesToQueryOperator; +use InvalidArgumentException; +use MongoDB\Builder\Expression\ResolvesToBool; -class AndQuery implements ResolvesToBoolExpression +use function count; +use function sprintf; + +class AndQuery implements ResolvesToBool { - /** @param list $query */ + /** @param list ...$query */ public array $query; - /** @param ResolvesToQueryOperator|array|object $query */ - public function __construct(array|object ...$query) + public function __construct(ResolvesToBool|bool ...$query) { + if (count($query) < 1) { + throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($query))); + } + $this->query = $query; } } diff --git a/src/Builder/Query/ExprQuery.php b/src/Builder/Query/ExprQuery.php index b9ff3f4f0..c002f386d 100644 --- a/src/Builder/Query/ExprQuery.php +++ b/src/Builder/Query/ExprQuery.php @@ -6,14 +6,13 @@ namespace MongoDB\Builder\Query; -use MongoDB\Builder\Expression\ResolvesToExpression; +use MongoDB\Builder\Expression\Expression; -class ExprQuery implements ResolvesToExpression +class ExprQuery implements Expression { - public array|bool|float|int|null|object|string $expression; + public mixed $expression; - /** @param ResolvesToExpression|array|bool|float|int|object|string|null $expression */ - public function __construct(array|bool|float|int|null|object|string $expression) + public function __construct(mixed $expression) { $this->expression = $expression; } diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index 6dc5064a9..41d2cd39b 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -6,41 +6,38 @@ namespace MongoDB\Builder; -use MongoDB\Builder\Expression\ResolvesToMatchExpression; -use MongoDB\Builder\Expression\ResolvesToSortSpecification; +use MongoDB\BSON\Document; +use MongoDB\BSON\Int64; +use MongoDB\BSON\Serializable; +use MongoDB\Builder\Expression\ResolvesToInt; +use MongoDB\Builder\Expression\ResolvesToObject; use MongoDB\Builder\Stage\LimitStage; use MongoDB\Builder\Stage\MatchStage; use MongoDB\Builder\Stage\SortStage; final class Stage { - /** - * This class cannot be instantiated. - */ - private function __construct() + /** @param Int64|ResolvesToInt|int $limit */ + public static function limit(Int64|ResolvesToInt|int $limit): LimitStage { + return new LimitStage($limit); } - /** @param ResolvesToMatchExpression|array|object $matchExpr */ - public static function match(array|object ...$matchExpr): Stage\MatchStage + public static function match(mixed ...$matchExpr): MatchStage { - return new MatchStage( - $matchExpr, - ); + return new MatchStage(...$matchExpr); } - /** @param ResolvesToSortSpecification|array|object $sortSpecification */ - public static function sort(array|object $sortSpecification): Stage\SortStage + /** @param Document|ResolvesToObject|Serializable|array|object $sortSpecification */ + public static function sort(array|object $sortSpecification): SortStage { - return new SortStage( - $sortSpecification, - ); + return new SortStage($sortSpecification); } - public static function limit(int $limit): Stage\LimitStage + /** + * This class cannot be instantiated. + */ + private function __construct() { - return new LimitStage( - $limit, - ); } } diff --git a/src/Builder/Stage/LimitStage.php b/src/Builder/Stage/LimitStage.php index 3a8dbf3bc..5f090456d 100644 --- a/src/Builder/Stage/LimitStage.php +++ b/src/Builder/Stage/LimitStage.php @@ -6,11 +6,15 @@ namespace MongoDB\Builder\Stage; +use MongoDB\BSON\Int64; +use MongoDB\Builder\Expression\ResolvesToInt; + class LimitStage { - public int $limit; + public Int64|ResolvesToInt|int $limit; - public function __construct(int $limit) + /** @param Int64|ResolvesToInt|int $limit */ + public function __construct(Int64|ResolvesToInt|int $limit) { $this->limit = $limit; } diff --git a/src/Builder/Stage/MatchStage.php b/src/Builder/Stage/MatchStage.php index 291c89e9a..0e323aa24 100644 --- a/src/Builder/Stage/MatchStage.php +++ b/src/Builder/Stage/MatchStage.php @@ -7,18 +7,17 @@ namespace MongoDB\Builder\Stage; use InvalidArgumentException; -use MongoDB\Builder\Expression\ResolvesToMatchExpression; +use MongoDB\Builder\Expression\Expression; use function count; use function sprintf; class MatchStage { - /** @param list $matchExpr */ + /** @param list ...$matchExpr */ public array $matchExpr; - /** @param ResolvesToMatchExpression|array|object $matchExpr */ - public function __construct(array|object ...$matchExpr) + public function __construct(mixed ...$matchExpr) { if (count($matchExpr) < 1) { throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($matchExpr))); diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php index 2979b99bd..d96aca16b 100644 --- a/src/Builder/Stage/SortStage.php +++ b/src/Builder/Stage/SortStage.php @@ -6,13 +6,15 @@ namespace MongoDB\Builder\Stage; -use MongoDB\Builder\Expression\ResolvesToSortSpecification; +use MongoDB\BSON\Document; +use MongoDB\BSON\Serializable; +use MongoDB\Builder\Expression\ResolvesToObject; class SortStage { public array|object $sortSpecification; - /** @param ResolvesToSortSpecification|array|object $sortSpecification */ + /** @param Document|ResolvesToObject|Serializable|array|object $sortSpecification */ public function __construct(array|object $sortSpecification) { $this->sortSpecification = $sortSpecification; From 2b60cf713b9928193fcc6829fd49cbfdb0da2e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 27 Sep 2023 20:04:22 +0200 Subject: [PATCH 10/31] Init BuilderCodec with a unit test --- generator/config/pipeline-operators.yaml | 10 ++ generator/config/query-operators.yaml | 21 +++ generator/config/stages.yaml | 19 ++- .../src/Definition/OperatorDefinition.php | 14 ++ generator/src/OperatorClassGenerator.php | 9 ++ src/Builder/Aggregation.php | 6 + src/Builder/Aggregation/AndAggregation.php | 3 + src/Builder/Aggregation/EqAggregation.php | 3 + src/Builder/Aggregation/FilterAggregation.php | 3 + src/Builder/Aggregation/GtAggregation.php | 3 + src/Builder/Aggregation/LtAggregation.php | 3 + src/Builder/Aggregation/NeAggregation.php | 3 + src/Builder/Aggregation/SumAggregation.php | 22 +++ src/Builder/BuilderCodec.php | 148 ++++++++++++++++++ src/Builder/Expression.php | 25 +++ src/Builder/Pipeline.php | 23 +++ src/Builder/Query.php | 24 +++ src/Builder/Query/AndQuery.php | 3 + src/Builder/Query/ExprQuery.php | 3 + src/Builder/Query/GtQuery.php | 22 +++ src/Builder/Query/GteQuery.php | 22 +++ src/Builder/Query/LtQuery.php | 22 +++ src/Builder/Query/OrQuery.php | 32 ++++ src/Builder/Stage.php | 15 +- src/Builder/Stage/GroupStage.php | 31 ++++ src/Builder/Stage/LimitStage.php | 5 +- src/Builder/Stage/MatchStage.php | 22 +-- src/Builder/Stage/SortStage.php | 5 +- src/Builder/Stage/Stage.php | 7 + tests/Builder/BuiderCodecTest.php | 68 ++++++++ 30 files changed, 574 insertions(+), 22 deletions(-) create mode 100644 src/Builder/Aggregation/SumAggregation.php create mode 100644 src/Builder/BuilderCodec.php create mode 100644 src/Builder/Expression.php create mode 100644 src/Builder/Pipeline.php create mode 100644 src/Builder/Query/GtQuery.php create mode 100644 src/Builder/Query/GteQuery.php create mode 100644 src/Builder/Query/LtQuery.php create mode 100644 src/Builder/Query/OrQuery.php create mode 100644 src/Builder/Stage/GroupStage.php create mode 100644 src/Builder/Stage/Stage.php create mode 100644 tests/Builder/BuiderCodecTest.php diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index 633150927..07a0dfed2 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -6,6 +6,7 @@ isVariadic: true variadicMin: 1 - name: eq + encode: array type: resolvesToBool args: - name: expression1 @@ -13,6 +14,7 @@ - name: expression2 type: expression - name: gt + encode: array type: resolvesToBool args: - name: expression1 @@ -20,6 +22,7 @@ - name: expression2 type: expression - name: lt + encode: array type: resolvesToBool args: - name: expression1 @@ -27,6 +30,7 @@ - name: expression2 type: expression - name: ne + encode: array type: resolvesToBool args: - name: expression1 @@ -34,6 +38,7 @@ - name: expression2 type: expression - name: filter + encode: object type: resolvesToArray usesNamedArgs: true args: @@ -47,3 +52,8 @@ - name: limit type: resolvesToInt isOptional: true +- name: sum + type: resolvesToInt + args: + - name: expression + type: expression \ No newline at end of file diff --git a/generator/config/query-operators.yaml b/generator/config/query-operators.yaml index a4a454756..639a4d77d 100644 --- a/generator/config/query-operators.yaml +++ b/generator/config/query-operators.yaml @@ -4,8 +4,29 @@ - name: query type: resolvesToBool isVariadic: true +- name: or + type: resolvesToBool + args: + - name: query + type: expression + isVariadic: true - name: expr type: expression args: - name: expression type: expression +- name: gt + type: resolvesToBool + args: + - name: value + type: expression +- name: lt + type: resolvesToBool + args: + - name: value + type: expression +- name: gte + type: resolvesToBool + args: + - name: value + type: expression \ No newline at end of file diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index e303a2236..a3b9f2926 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -1,14 +1,27 @@ - name: match + type: stage args: - - name: matchExpr + - name: query type: expression - isVariadic: true - variadicMin: 1 - name: sort + type: stage args: - name: sortSpecification type: resolvesToObject - name: limit + type: stage args: - name: limit type: resolvesToInt +- name: group + type: stage + encode: object + args: + # @todo _id is not optional but only nullable + - name: _id + type: expression + isOptional: true + # @todo fields are optional and encoded at the same level as _id + - name: fields + type: resolvesToObject + isOptional: true diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index f9537ab0e..5bf42a478 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -3,7 +3,12 @@ namespace MongoDB\CodeGenerator\Definition; +use InvalidArgumentException; + use function array_map; +use function count; +use function in_array; +use function sprintf; final readonly class OperatorDefinition { @@ -12,10 +17,19 @@ public function __construct( public string $name, + public ?string $encode = null, public ?string $type = null, public bool $usesNamedArgs = false, array $args = [], ) { + if ($encode === null && count($args) !== 1) { + throw new InvalidArgumentException(sprintf('Operator "%s" have %s arguments, the "encode" parameter must be specified.', $this->name, count($args))); + } + + if (! in_array($this->encode, [null, 'array', 'object'], true)) { + throw new InvalidArgumentException(sprintf('Operator "%s" expect "encode" value to be "array" or "object". Got "%s".', $this->name, $this->encode)); + } + $this->arguments = array_map( fn ($arg): ArgumentDefinition => new ArgumentDefinition(...$arg), $args, diff --git a/generator/src/OperatorClassGenerator.php b/generator/src/OperatorClassGenerator.php index 3615959c5..375fab20c 100644 --- a/generator/src/OperatorClassGenerator.php +++ b/generator/src/OperatorClassGenerator.php @@ -3,6 +3,7 @@ namespace MongoDB\CodeGenerator; +use MongoDB\Builder\Stage\Stage; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; use Nette\PhpGenerator\PhpNamespace; @@ -29,6 +30,10 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition $class = $namespace->addClass($this->getOperatorClassName($definition, $operator)); $class->setImplements($this->getInterfaces($operator)); + // Expose operator metadata as constants + $class->addConstant('NAME', '$' . $operator->name); + $class->addConstant('ENCODE', $operator->encode ?? 'single'); + $constuctor = $class->addMethod('__construct'); foreach ($operator->arguments as $argument) { $type = $this->generateExpressionTypes($argument); @@ -93,6 +98,10 @@ private function getInterfaces(OperatorDefinition $definition): array return []; } + if ($definition->type === 'stage') { + return ['\\' . Stage::class]; + } + $interface = $this->getExpressionTypeInterface($definition->type); if (! interface_exists($interface)) { throw new RuntimeException(sprintf('"%s" is not an interface.', $interface)); diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php index 5ab622417..91eb72097 100644 --- a/src/Builder/Aggregation.php +++ b/src/Builder/Aggregation.php @@ -14,6 +14,7 @@ use MongoDB\Builder\Aggregation\GtAggregation; use MongoDB\Builder\Aggregation\LtAggregation; use MongoDB\Builder\Aggregation\NeAggregation; +use MongoDB\Builder\Aggregation\SumAggregation; use MongoDB\Builder\Expression\Expression; use MongoDB\Builder\Expression\ResolvesToArray; use MongoDB\Builder\Expression\ResolvesToBool; @@ -62,6 +63,11 @@ public static function ne(mixed $expression1, mixed $expression2): NeAggregation return new NeAggregation($expression1, $expression2); } + public static function sum(mixed $expression): SumAggregation + { + return new SumAggregation($expression); + } + /** * This class cannot be instantiated. */ diff --git a/src/Builder/Aggregation/AndAggregation.php b/src/Builder/Aggregation/AndAggregation.php index 55e20e223..bf9592ce5 100644 --- a/src/Builder/Aggregation/AndAggregation.php +++ b/src/Builder/Aggregation/AndAggregation.php @@ -15,6 +15,9 @@ class AndAggregation implements ResolvesToBool { + public const NAME = '$and'; + public const ENCODE = 'single'; + /** @param list ...$expressions */ public array $expressions; diff --git a/src/Builder/Aggregation/EqAggregation.php b/src/Builder/Aggregation/EqAggregation.php index 92f487e2c..4ca188690 100644 --- a/src/Builder/Aggregation/EqAggregation.php +++ b/src/Builder/Aggregation/EqAggregation.php @@ -10,6 +10,9 @@ class EqAggregation implements ResolvesToBool { + public const NAME = '$eq'; + public const ENCODE = 'array'; + public mixed $expression1; public mixed $expression2; diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php index 9ca99b0f0..5d0febdec 100644 --- a/src/Builder/Aggregation/FilterAggregation.php +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -22,6 +22,9 @@ class FilterAggregation implements ResolvesToArray { + public const NAME = '$filter'; + public const ENCODE = 'object'; + public PackedArray|ResolvesToArray|BSONArray|array $input; public ResolvesToBool|bool $cond; public ResolvesToString|null|string $as; diff --git a/src/Builder/Aggregation/GtAggregation.php b/src/Builder/Aggregation/GtAggregation.php index 0817e3496..5b4a70563 100644 --- a/src/Builder/Aggregation/GtAggregation.php +++ b/src/Builder/Aggregation/GtAggregation.php @@ -10,6 +10,9 @@ class GtAggregation implements ResolvesToBool { + public const NAME = '$gt'; + public const ENCODE = 'array'; + public mixed $expression1; public mixed $expression2; diff --git a/src/Builder/Aggregation/LtAggregation.php b/src/Builder/Aggregation/LtAggregation.php index d2271e68d..45c0d0fa0 100644 --- a/src/Builder/Aggregation/LtAggregation.php +++ b/src/Builder/Aggregation/LtAggregation.php @@ -10,6 +10,9 @@ class LtAggregation implements ResolvesToBool { + public const NAME = '$lt'; + public const ENCODE = 'array'; + public mixed $expression1; public mixed $expression2; diff --git a/src/Builder/Aggregation/NeAggregation.php b/src/Builder/Aggregation/NeAggregation.php index 849885c70..d25524369 100644 --- a/src/Builder/Aggregation/NeAggregation.php +++ b/src/Builder/Aggregation/NeAggregation.php @@ -10,6 +10,9 @@ class NeAggregation implements ResolvesToBool { + public const NAME = '$ne'; + public const ENCODE = 'array'; + public mixed $expression1; public mixed $expression2; diff --git a/src/Builder/Aggregation/SumAggregation.php b/src/Builder/Aggregation/SumAggregation.php new file mode 100644 index 000000000..cc17f006c --- /dev/null +++ b/src/Builder/Aggregation/SumAggregation.php @@ -0,0 +1,22 @@ +expression = $expression; + } +} diff --git a/src/Builder/BuilderCodec.php b/src/Builder/BuilderCodec.php new file mode 100644 index 000000000..31f23795a --- /dev/null +++ b/src/Builder/BuilderCodec.php @@ -0,0 +1,148 @@ +canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + // A pipeline is encoded as a list of stages + if ($value instanceof Pipeline) { + $encoded = []; + foreach ($value->stages as $stage) { + $encoded[] = $this->encodeIfSupported($stage); + } + + return $encoded; + } + + // This specific encoding code if temporary until we have a generic way to encode stages and operators + if ($value instanceof GroupStage) { + $result = new stdClass(); + $result->_id = $this->encodeIfSupported($value->_id); + // Specific: fields are encoded as a map of properties to their values at the top level as _id + foreach ($value->fields as $key => $val) { + $result->{$key} = $this->encodeIfSupported($val); + } + + return (object) [$value::NAME => $result]; + } + + if ($value instanceof OrQuery) { + $result = []; + foreach ($value->query as $query) { + $encodedQuery = new stdClass(); + foreach ($query as $field => $expression) { + // Specific: $or queries are encoded as a list of expressions + // We need to merge query expressions into a single object + if (is_array($expression) && array_is_list($expression)) { + $mergedExpressions = []; + foreach ($expression as $expr) { + $mergedExpressions = array_merge($mergedExpressions, (array) $this->encodeIfSupported($expr)); + } + + $encodedQuery->{$field} = (object) $mergedExpressions; + } else { + $encodedQuery->{$field} = $this->encodeIfSupported($expression); + } + } + + $result[] = $encodedQuery; + } + + return (object) [$value::NAME => $result]; + } + + // The generic but incomplete encoding code + switch ($value::ENCODE) { + case self::ENCODE_AS_SINGLE: + return $this->encodeAsSingle($value); + + case self::ENCODE_AS_ARRAY: + return $this->encodeAsArray($value); + + case self::ENCODE_AS_OBJECT: + return $this->encodeAsObject($value); + } + + throw new LogicException(sprintf('Class "%s" does not have a valid ENCODE constant.', $value::class)); + } + + private function encodeAsSingle($value): stdClass + { + $result = []; + foreach ($value as $val) { + $result = $this->encodeIfSupported($val); + break; + } + + return (object) [$value::NAME => $result]; + } + + private function encodeAsArray($value): stdClass + { + $result = []; + foreach ($value as $val) { + $result[] = $this->encodeIfSupported($val); + } + + return (object) [$value::NAME => $result]; + } + + private function encodeAsObject($value): stdClass + { + $result = new stdClass(); + foreach ($value as $key => $val) { + $result->{'$' . $key} = $this->encodeIfSupported($val); + } + + return (object) [$value::NAME => $result]; + } +} diff --git a/src/Builder/Expression.php b/src/Builder/Expression.php new file mode 100644 index 000000000..80825d5c4 --- /dev/null +++ b/src/Builder/Expression.php @@ -0,0 +1,25 @@ +stages = $stages; + } +} diff --git a/src/Builder/Query.php b/src/Builder/Query.php index 50b8410bd..7d0720c16 100644 --- a/src/Builder/Query.php +++ b/src/Builder/Query.php @@ -9,6 +9,10 @@ use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Query\AndQuery; use MongoDB\Builder\Query\ExprQuery; +use MongoDB\Builder\Query\GteQuery; +use MongoDB\Builder\Query\GtQuery; +use MongoDB\Builder\Query\LtQuery; +use MongoDB\Builder\Query\OrQuery; final class Query { @@ -22,6 +26,26 @@ public static function expr(mixed $expression): ExprQuery return new ExprQuery($expression); } + public static function gt(mixed $value): GtQuery + { + return new GtQuery($value); + } + + public static function gte(mixed $value): GteQuery + { + return new GteQuery($value); + } + + public static function lt(mixed $value): LtQuery + { + return new LtQuery($value); + } + + public static function or(mixed ...$query): OrQuery + { + return new OrQuery(...$query); + } + /** * This class cannot be instantiated. */ diff --git a/src/Builder/Query/AndQuery.php b/src/Builder/Query/AndQuery.php index eed3372e5..2d4337fac 100644 --- a/src/Builder/Query/AndQuery.php +++ b/src/Builder/Query/AndQuery.php @@ -14,6 +14,9 @@ class AndQuery implements ResolvesToBool { + public const NAME = '$and'; + public const ENCODE = 'single'; + /** @param list ...$query */ public array $query; diff --git a/src/Builder/Query/ExprQuery.php b/src/Builder/Query/ExprQuery.php index c002f386d..3b505bd76 100644 --- a/src/Builder/Query/ExprQuery.php +++ b/src/Builder/Query/ExprQuery.php @@ -10,6 +10,9 @@ class ExprQuery implements Expression { + public const NAME = '$expr'; + public const ENCODE = 'single'; + public mixed $expression; public function __construct(mixed $expression) diff --git a/src/Builder/Query/GtQuery.php b/src/Builder/Query/GtQuery.php new file mode 100644 index 000000000..0a5e5b702 --- /dev/null +++ b/src/Builder/Query/GtQuery.php @@ -0,0 +1,22 @@ +value = $value; + } +} diff --git a/src/Builder/Query/GteQuery.php b/src/Builder/Query/GteQuery.php new file mode 100644 index 000000000..124146638 --- /dev/null +++ b/src/Builder/Query/GteQuery.php @@ -0,0 +1,22 @@ +value = $value; + } +} diff --git a/src/Builder/Query/LtQuery.php b/src/Builder/Query/LtQuery.php new file mode 100644 index 000000000..a8630bea7 --- /dev/null +++ b/src/Builder/Query/LtQuery.php @@ -0,0 +1,22 @@ +value = $value; + } +} diff --git a/src/Builder/Query/OrQuery.php b/src/Builder/Query/OrQuery.php new file mode 100644 index 000000000..49434277d --- /dev/null +++ b/src/Builder/Query/OrQuery.php @@ -0,0 +1,32 @@ + ...$query */ + public array $query; + + public function __construct(mixed ...$query) + { + if (count($query) < 1) { + throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($query))); + } + + $this->query = $query; + } +} diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index 41d2cd39b..b68e2c116 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -9,23 +9,34 @@ use MongoDB\BSON\Document; use MongoDB\BSON\Int64; use MongoDB\BSON\Serializable; +use MongoDB\Builder\Expression\Expression; use MongoDB\Builder\Expression\ResolvesToInt; use MongoDB\Builder\Expression\ResolvesToObject; +use MongoDB\Builder\Stage\GroupStage; use MongoDB\Builder\Stage\LimitStage; use MongoDB\Builder\Stage\MatchStage; use MongoDB\Builder\Stage\SortStage; final class Stage { + /** + * @param Expression|mixed|null $_id + * @param Document|ResolvesToObject|Serializable|array|object|null $fields + */ + public static function group(mixed $_id = null, array|null|object $fields = null): GroupStage + { + return new GroupStage($_id, $fields); + } + /** @param Int64|ResolvesToInt|int $limit */ public static function limit(Int64|ResolvesToInt|int $limit): LimitStage { return new LimitStage($limit); } - public static function match(mixed ...$matchExpr): MatchStage + public static function match(mixed $query): MatchStage { - return new MatchStage(...$matchExpr); + return new MatchStage($query); } /** @param Document|ResolvesToObject|Serializable|array|object $sortSpecification */ diff --git a/src/Builder/Stage/GroupStage.php b/src/Builder/Stage/GroupStage.php new file mode 100644 index 000000000..f085736bd --- /dev/null +++ b/src/Builder/Stage/GroupStage.php @@ -0,0 +1,31 @@ +_id = $_id; + $this->fields = $fields; + } +} diff --git a/src/Builder/Stage/LimitStage.php b/src/Builder/Stage/LimitStage.php index 5f090456d..72184a8fa 100644 --- a/src/Builder/Stage/LimitStage.php +++ b/src/Builder/Stage/LimitStage.php @@ -9,8 +9,11 @@ use MongoDB\BSON\Int64; use MongoDB\Builder\Expression\ResolvesToInt; -class LimitStage +class LimitStage implements Stage { + public const NAME = '$limit'; + public const ENCODE = 'single'; + public Int64|ResolvesToInt|int $limit; /** @param Int64|ResolvesToInt|int $limit */ diff --git a/src/Builder/Stage/MatchStage.php b/src/Builder/Stage/MatchStage.php index 0e323aa24..e2026c102 100644 --- a/src/Builder/Stage/MatchStage.php +++ b/src/Builder/Stage/MatchStage.php @@ -6,23 +6,15 @@ namespace MongoDB\Builder\Stage; -use InvalidArgumentException; -use MongoDB\Builder\Expression\Expression; - -use function count; -use function sprintf; - -class MatchStage +class MatchStage implements Stage { - /** @param list ...$matchExpr */ - public array $matchExpr; + public const NAME = '$match'; + public const ENCODE = 'single'; - public function __construct(mixed ...$matchExpr) - { - if (count($matchExpr) < 1) { - throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($matchExpr))); - } + public mixed $query; - $this->matchExpr = $matchExpr; + public function __construct(mixed $query) + { + $this->query = $query; } } diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php index d96aca16b..29e72fd73 100644 --- a/src/Builder/Stage/SortStage.php +++ b/src/Builder/Stage/SortStage.php @@ -10,8 +10,11 @@ use MongoDB\BSON\Serializable; use MongoDB\Builder\Expression\ResolvesToObject; -class SortStage +class SortStage implements Stage { + public const NAME = '$sort'; + public const ENCODE = 'single'; + public array|object $sortSpecification; /** @param Document|ResolvesToObject|Serializable|array|object $sortSpecification */ diff --git a/src/Builder/Stage/Stage.php b/src/Builder/Stage/Stage.php new file mode 100644 index 000000000..7893b937a --- /dev/null +++ b/src/Builder/Stage/Stage.php @@ -0,0 +1,7 @@ + (object) ['$eq' => ['$foo', 1]]], + (object) ['$match' => (object) ['$ne' => ['$foo', 2]]], + (object) ['$limit' => 1], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#perform-a-count */ + public function testPerformCount(): void + { + $pipeline = new Pipeline( + Stage::match(Query::or( + ['score' => [Query::gt(70), Query::lt(90)]], + ['views' => Query::gte(1000)], + )), + Stage::group(null, ['count' => Aggregation::sum(1)]), + ); + + $expected = [ + (object) [ + '$match' => (object) [ + '$or' => [ + (object) ['score' => (object) ['$gt' => 70, '$lt' => 90]], + (object) ['views' => (object) ['$gte' => 1000]], + ], + ], + ], + (object) [ + '$group' => (object) [ + '_id' => null, + 'count' => (object) ['$sum' => 1], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + private static function assertSamePipeline(array $expected, Pipeline $actual): void + { + $codec = new BuilderCodec(); + + self::assertEquals($expected, $codec->encode($actual)); + } +} From 02ae0842018a73fe5955d29c224e1fbff3e1635f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 27 Sep 2023 20:42:45 +0200 Subject: [PATCH 11/31] Cleanup --- .gitattributes | 1 + generator/README.md | 4 ++-- generator/config/operators.php | 12 +----------- generator/config/pipeline-operators.yaml | 2 +- generator/config/query-operators.yaml | 2 +- generator/src/AbstractGenerator.php | 10 +++++++--- generator/src/Definition/GeneratorDefinition.php | 5 ----- generator/src/OperatorGenerator.php | 1 + 8 files changed, 14 insertions(+), 23 deletions(-) diff --git a/.gitattributes b/.gitattributes index 7cfb8f22f..0159e364a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,3 +25,4 @@ psalm-baseline.xml export-ignore /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 diff --git a/generator/README.md b/generator/README.md index 73bae9fad..f77d20cfe 100644 --- a/generator/README.md +++ b/generator/README.md @@ -5,7 +5,7 @@ The `generator` directory is not included in `mongodb/mongodb` package and is no ## Contributing -Updating the generated code can be done only by modifying the generator code, or its configuration. +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. @@ -16,4 +16,4 @@ To run the generator, you need to have PHP 8.2+ installed and Composer. ## Configuration -The `generator/config/*.yaml` files contains the list of operations that are supported by the library. +The `generator/config/*.yaml` files contains the list of operators and stages that are supported by the library. diff --git a/generator/config/operators.php b/generator/config/operators.php index 68709cb67..994919b4e 100644 --- a/generator/config/operators.php +++ b/generator/config/operators.php @@ -2,12 +2,8 @@ namespace MongoDB\CodeGenerator\Config; -use MongoDB\CodeGenerator\ExpressionClassGenerator; -use MongoDB\CodeGenerator\OperatorFactoryGenerator; use MongoDB\CodeGenerator\OperatorClassGenerator; - -$src = __DIR__ . '/../../src'; -$tests = __DIR__ . '/../../tests'; +use MongoDB\CodeGenerator\OperatorFactoryGenerator; return [ // Aggregation Pipeline Stages @@ -15,14 +11,12 @@ 'configFile' => __DIR__ . '/stages.yaml', 'generatorClass' => OperatorClassGenerator::class, 'namespace' => 'MongoDB\\Builder\\Stage', - 'filePath' => $src . '/Builder/Stage', 'classNameSuffix' => 'Stage', ], [ 'configFile' => __DIR__ . '/stages.yaml', 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Stage', - 'filePath' => $src . '/Builder/Stage', 'classNameSuffix' => 'Stage', ], @@ -31,14 +25,12 @@ 'configFile' => __DIR__ . '/pipeline-operators.yaml', 'generatorClass' => OperatorClassGenerator::class, 'namespace' => 'MongoDB\\Builder\\Aggregation', - 'filePath' => $src . '/Builder/Aggregation', 'classNameSuffix' => 'Aggregation', ], [ 'configFile' => __DIR__ . '/pipeline-operators.yaml', 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Aggregation', - 'filePath' => $src . '/Builder/Aggregation', 'classNameSuffix' => 'Aggregation', ], @@ -47,14 +39,12 @@ 'configFile' => __DIR__ . '/query-operators.yaml', 'generatorClass' => OperatorClassGenerator::class, 'namespace' => 'MongoDB\\Builder\\Query', - 'filePath' => $src . '/Builder/Query', 'classNameSuffix' => 'Query', ], [ 'configFile' => __DIR__ . '/query-operators.yaml', 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Query', - 'filePath' => $src . '/Builder/Query', 'classNameSuffix' => 'Query', ], ]; diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index 07a0dfed2..ea47c00c1 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -56,4 +56,4 @@ type: resolvesToInt args: - name: expression - type: expression \ No newline at end of file + type: expression diff --git a/generator/config/query-operators.yaml b/generator/config/query-operators.yaml index 639a4d77d..433038f3f 100644 --- a/generator/config/query-operators.yaml +++ b/generator/config/query-operators.yaml @@ -29,4 +29,4 @@ type: resolvesToBool args: - name: value - type: expression \ No newline at end of file + type: expression diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index f4e229660..02fb6f0b9 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -22,7 +22,6 @@ use function str_replace; use function str_starts_with; -/** @internal */ abstract class AbstractGenerator { protected Printer $printer; @@ -33,6 +32,11 @@ public function __construct( $this->printer = new PsrPrinter(); } + /** + * Split the namespace and class name from a fully qualified class name. + * + * @return array{0: string, 1: string} + */ final protected function splitNamespaceAndClassName(string $fqcn): array { $parts = explode('\\', $fqcn); @@ -41,7 +45,7 @@ final protected function splitNamespaceAndClassName(string $fqcn): array return [implode('\\', $parts), $className]; } - protected function writeFile(PhpNamespace $namespace): void + final protected function writeFile(PhpNamespace $namespace): void { $classes = $namespace->getClasses(); if (count($classes) !== 1) { @@ -72,7 +76,7 @@ private function getFileName(string ...$fqcn): string { $fqcn = implode('\\', $fqcn); - // Config from composer.json + // Config from composer.json autoload $config = [ 'MongoDB\\Tests\\' => 'tests/', 'MongoDB\\' => 'src/', diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index 6b0ce038a..ad1f40f97 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -21,15 +21,10 @@ public function __construct( */ public readonly string $generatorClass, public readonly string $namespace, - public readonly string $filePath, public readonly string $classNameSuffix = '', public readonly array $interfaces = [], public readonly ?string $parentClass = null, ) { - if (str_ends_with($this->filePath, '/')) { - throw new InvalidArgumentException(sprintf('File path must not end with "/". Got "%s".', $this->filePath)); - } - if (! str_starts_with($this->namespace, 'MongoDB\\')) { throw new InvalidArgumentException(sprintf('Namespace must start with "MongoDB\\". Got "%s".', $this->namespace)); } diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index d0eb56de2..6ee40d8de 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -1,4 +1,5 @@ Date: Wed, 27 Sep 2023 22:11:09 +0200 Subject: [PATCH 12/31] Merge multiple arg types --- generator/config/expressions.php | 2 + generator/config/pipeline-operators.yaml | 10 +++- generator/config/stages.yaml | 5 ++ .../src/Definition/ArgumentDefinition.php | 15 ++++- generator/src/OperatorGenerator.php | 11 +++- src/Builder/Aggregation.php | 15 +++-- src/Builder/Aggregation/FilterAggregation.php | 11 ++-- src/Builder/Aggregation/GteAggregation.php | 24 ++++++++ src/Builder/BuilderCodec.php | 28 +++++++++- src/Builder/Expression.php | 13 ++++- src/Builder/Stage.php | 7 +++ src/Builder/Stage/ProjectStage.php | 25 +++++++++ tests/Builder/BuiderCodecTest.php | 56 ++++++++++++++++++- 13 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 src/Builder/Aggregation/GteAggregation.php create mode 100644 src/Builder/Stage/ProjectStage.php diff --git a/generator/config/expressions.php b/generator/config/expressions.php index 08a99f969..f0c65f94f 100644 --- a/generator/config/expressions.php +++ b/generator/config/expressions.php @@ -12,11 +12,13 @@ Expression::class => [ 'types' => ['mixed'], ], + // @todo if replaced by a string, it must start with $ FieldPath::class => [ 'class' => true, 'implements' => [Expression::class], 'types' => ['string'], ], + // @todo if replaced by a string, it must start with $$ Variable::class => [ 'class' => true, 'implements' => [Expression::class], diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index ea47c00c1..b38a76bb7 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -21,6 +21,14 @@ type: expression - name: expression2 type: expression +- name: gte + encode: array + type: resolvesToBool + args: + - name: expression1 + type: expression + - name: expression2 + type: expression - name: lt encode: array type: resolvesToBool @@ -43,7 +51,7 @@ usesNamedArgs: true args: - name: input - type: resolvesToArray + type: [resolvesToArray, fieldPath] - name: cond type: resolvesToBool - name: as diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index a3b9f2926..26ca19944 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -25,3 +25,8 @@ - name: fields type: resolvesToObject isOptional: true +- name: project + type: stage + args: + - name: specifications + type: resolvesToObject diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index 2d120ee83..e004a7c1b 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -3,14 +3,27 @@ namespace MongoDB\CodeGenerator\Definition; +use InvalidArgumentException; + +use function is_array; +use function is_string; + final readonly class ArgumentDefinition { public function __construct( public string $name, - public string $type, + /** @var string|list */ + public string|array $type, public bool $isOptional = false, public bool $isVariadic = false, public int $variadicMin = 1, ) { + if (is_array($type)) { + foreach ($type as $t) { + if (! is_string($t)) { + throw new InvalidArgumentException('Argument type must be a string or list of strings'); + } + } + } } } diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index 6ee40d8de..c938331bb 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -51,7 +51,7 @@ final protected function getExpressionTypeInterface(string $type): string { $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($type); - if (! interface_exists($interface) || ! is_subclass_of($interface, Expression::class) && $interface !== Expression::class) { + if ($interface !== Expression::class && ! is_subclass_of($interface, Expression::class)) { throw new InvalidArgumentException(sprintf('Invalid expression type "%s".', $type)); } @@ -65,8 +65,13 @@ final protected function getExpressionTypeInterface(string $type): string */ final protected function generateExpressionTypes(ArgumentDefinition $arg): object { - $interface = $this->getExpressionTypeInterface($arg->type); - $docTypes = $nativeTypes = array_merge([$interface], $interface::ACCEPTED_TYPES); + $nativeTypes = []; + foreach ((array) $arg->type as $type) { + $interface = $this->getExpressionTypeInterface($type); + $nativeTypes = array_merge($nativeTypes, [$interface], $interface::ACCEPTED_TYPES); + } + + $docTypes = $nativeTypes = array_unique($nativeTypes); $listCheck = false; $use = []; diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php index 91eb72097..893a830ba 100644 --- a/src/Builder/Aggregation.php +++ b/src/Builder/Aggregation.php @@ -12,10 +12,12 @@ use MongoDB\Builder\Aggregation\EqAggregation; use MongoDB\Builder\Aggregation\FilterAggregation; use MongoDB\Builder\Aggregation\GtAggregation; +use MongoDB\Builder\Aggregation\GteAggregation; use MongoDB\Builder\Aggregation\LtAggregation; use MongoDB\Builder\Aggregation\NeAggregation; use MongoDB\Builder\Aggregation\SumAggregation; use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\FieldPath; use MongoDB\Builder\Expression\ResolvesToArray; use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Expression\ResolvesToInt; @@ -35,12 +37,12 @@ public static function eq(mixed $expression1, mixed $expression2): EqAggregation } /** - * @param BSONArray|PackedArray|ResolvesToArray|list $input - * @param ResolvesToString|string|null $as - * @param Int64|ResolvesToInt|int|null $limit + * @param BSONArray|FieldPath|PackedArray|ResolvesToArray|list|string $input + * @param ResolvesToString|string|null $as + * @param Int64|ResolvesToInt|int|null $limit */ public static function filter( - PackedArray|ResolvesToArray|BSONArray|array $input, + PackedArray|FieldPath|ResolvesToArray|BSONArray|array|string $input, ResolvesToBool|bool $cond, ResolvesToString|null|string $as = null, Int64|ResolvesToInt|int|null $limit = null, @@ -53,6 +55,11 @@ public static function gt(mixed $expression1, mixed $expression2): GtAggregation return new GtAggregation($expression1, $expression2); } + public static function gte(mixed $expression1, mixed $expression2): GteAggregation + { + return new GteAggregation($expression1, $expression2); + } + public static function lt(mixed $expression1, mixed $expression2): LtAggregation { return new LtAggregation($expression1, $expression2); diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php index 5d0febdec..3524be210 100644 --- a/src/Builder/Aggregation/FilterAggregation.php +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -10,6 +10,7 @@ use MongoDB\BSON\Int64; use MongoDB\BSON\PackedArray; use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\FieldPath; use MongoDB\Builder\Expression\ResolvesToArray; use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Expression\ResolvesToInt; @@ -25,18 +26,18 @@ class FilterAggregation implements ResolvesToArray public const NAME = '$filter'; public const ENCODE = 'object'; - public PackedArray|ResolvesToArray|BSONArray|array $input; + public PackedArray|FieldPath|ResolvesToArray|BSONArray|array|string $input; public ResolvesToBool|bool $cond; public ResolvesToString|null|string $as; public Int64|ResolvesToInt|int|null $limit; /** - * @param BSONArray|PackedArray|ResolvesToArray|list $input - * @param ResolvesToString|string|null $as - * @param Int64|ResolvesToInt|int|null $limit + * @param BSONArray|FieldPath|PackedArray|ResolvesToArray|list|string $input + * @param ResolvesToString|string|null $as + * @param Int64|ResolvesToInt|int|null $limit */ public function __construct( - PackedArray|ResolvesToArray|BSONArray|array $input, + PackedArray|FieldPath|ResolvesToArray|BSONArray|array|string $input, ResolvesToBool|bool $cond, ResolvesToString|null|string $as = null, Int64|ResolvesToInt|int|null $limit = null, diff --git a/src/Builder/Aggregation/GteAggregation.php b/src/Builder/Aggregation/GteAggregation.php new file mode 100644 index 000000000..5142eb275 --- /dev/null +++ b/src/Builder/Aggregation/GteAggregation.php @@ -0,0 +1,24 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } +} diff --git a/src/Builder/BuilderCodec.php b/src/Builder/BuilderCodec.php index 31f23795a..082354a3a 100644 --- a/src/Builder/BuilderCodec.php +++ b/src/Builder/BuilderCodec.php @@ -4,8 +4,11 @@ use LogicException; use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\FieldPath; +use MongoDB\Builder\Expression\Variable; use MongoDB\Builder\Query\OrQuery; use MongoDB\Builder\Stage\GroupStage; +use MongoDB\Builder\Stage\ProjectStage; use MongoDB\Builder\Stage\Stage; use MongoDB\Codec\Codec; use MongoDB\Codec\DecodeIfSupported; @@ -47,7 +50,7 @@ public function decode($value) throw UnsupportedValueException::invalidDecodableValue($value); } - public function encode($value): array|stdClass + public function encode($value): array|stdClass|string|int|float|bool|null { if (! $this->canEncode($value)) { throw UnsupportedValueException::invalidEncodableValue($value); @@ -64,6 +67,14 @@ public function encode($value): array|stdClass } // This specific encoding code if temporary until we have a generic way to encode stages and operators + if ($value instanceof FieldPath) { + return '$' . $value->expression; + } + + if ($value instanceof Variable) { + return '$$' . $value->expression; + } + if ($value instanceof GroupStage) { $result = new stdClass(); $result->_id = $this->encodeIfSupported($value->_id); @@ -75,6 +86,16 @@ public function encode($value): array|stdClass return (object) [$value::NAME => $result]; } + if ($value instanceof ProjectStage) { + $result = new stdClass(); + // Specific: fields are encoded as a map of properties to their values at the top level as _id + foreach ($value->specifications as $key => $val) { + $result->{$key} = $this->encodeIfSupported($val); + } + + return (object) [$value::NAME => $result]; + } + if ($value instanceof OrQuery) { $result = []; foreach ($value->query as $query) { @@ -140,7 +161,10 @@ private function encodeAsObject($value): stdClass { $result = new stdClass(); foreach ($value as $key => $val) { - $result->{'$' . $key} = $this->encodeIfSupported($val); + $val = $this->encodeIfSupported($val); + if ($val !== null) { + $result->{$key} = $val; + } } return (object) [$value::NAME => $result]; diff --git a/src/Builder/Expression.php b/src/Builder/Expression.php index 80825d5c4..e32021057 100644 --- a/src/Builder/Expression.php +++ b/src/Builder/Expression.php @@ -8,18 +8,25 @@ class Expression { - public function fieldPath(string $path): FieldPath + public static function fieldPath(string $path): FieldPath { return new FieldPath($path); } - public function object(array $args): stdClass + public static function object(array $args): stdClass { return (object) $args; } - public function variable(string $name): Variable + public static function variable(string $name): Variable { return new Variable($name); } + + /** + * This class cannot be instantiated. + */ + private function __construct() + { + } } diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index b68e2c116..857ef3f45 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -15,6 +15,7 @@ use MongoDB\Builder\Stage\GroupStage; use MongoDB\Builder\Stage\LimitStage; use MongoDB\Builder\Stage\MatchStage; +use MongoDB\Builder\Stage\ProjectStage; use MongoDB\Builder\Stage\SortStage; final class Stage @@ -39,6 +40,12 @@ public static function match(mixed $query): MatchStage return new MatchStage($query); } + /** @param Document|ResolvesToObject|Serializable|array|object $specifications */ + public static function project(array|object $specifications): ProjectStage + { + return new ProjectStage($specifications); + } + /** @param Document|ResolvesToObject|Serializable|array|object $sortSpecification */ public static function sort(array|object $sortSpecification): SortStage { diff --git a/src/Builder/Stage/ProjectStage.php b/src/Builder/Stage/ProjectStage.php new file mode 100644 index 000000000..305f481c1 --- /dev/null +++ b/src/Builder/Stage/ProjectStage.php @@ -0,0 +1,25 @@ +specifications = $specifications; + } +} diff --git a/tests/Builder/BuiderCodecTest.php b/tests/Builder/BuiderCodecTest.php index bfbc3181d..cb3f9ead3 100644 --- a/tests/Builder/BuiderCodecTest.php +++ b/tests/Builder/BuiderCodecTest.php @@ -2,13 +2,17 @@ namespace MongoDB\Tests\Builder; +use Generator; use MongoDB\Builder\Aggregation; use MongoDB\Builder\BuilderCodec; +use MongoDB\Builder\Expression; use MongoDB\Builder\Pipeline; use MongoDB\Builder\Query; use MongoDB\Builder\Stage; use MongoDB\Tests\TestCase; +use function array_merge; + class BuiderCodecTest extends TestCase { public function testPipeline(): void @@ -59,10 +63,58 @@ public function testPerformCount(): void $this->assertSamePipeline($expected, $pipeline); } - private static function assertSamePipeline(array $expected, Pipeline $actual): void + /** + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/#examples + * @dataProvider provideAggregationFilterLimit + */ + public function testAggregationFilter($limit, $expectedLimit): void + { + $pipeline = new Pipeline( + Stage::project([ + 'items' => Aggregation::filter( + input: Expression::fieldPath('items'), + cond: Aggregation::gte(Expression::variable('item.price'), 100), + as: 'item', + limit: $limit, + ), + ]), + ); + + $expected = [ + (object) [ + '$project' => (object) [ + 'items' => (object) [ + '$filter' => (object) array_merge([ + 'input' => '$items', + 'as' => 'item', + 'cond' => (object) ['$gte' => ['$$item.price', 100]], + ], $expectedLimit), + ], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + public static function provideAggregationFilterLimit(): Generator + { + yield 'unspecified limit' => [ + null, + [], + ]; + + yield 'int limit' => [ + 1, + ['limit' => 1], + ]; + } + + private static function assertSamePipeline(array $expected, Pipeline $pipeline): void { $codec = new BuilderCodec(); + $actual = $codec->encode($pipeline); - self::assertEquals($expected, $codec->encode($actual)); + self::assertEquals($expected, $actual); } } From d0ef3bb16fe05ec58b317bca302b4f88d46dd6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 28 Sep 2023 19:25:48 +0200 Subject: [PATCH 13/31] Add typed FieldPath classes Exclude generated files from phpcs Add Interface suffix to interfaces to prevent confusion with factories Update codec to be encoder only --- generator/README.md | 1 - generator/config/expressions.php | 51 ++++++++---- generator/config/pipeline-operators.yaml | 3 +- generator/src/Command/GenerateCommand.php | 7 +- .../src/Definition/ExpressionDefinition.php | 6 ++ .../src/Definition/OperatorDefinition.php | 1 - generator/src/ExpressionClassGenerator.php | 11 ++- generator/src/ExpressionFactoryGenerator.php | 51 ++++++++++++ generator/src/OperatorClassGenerator.php | 13 ++- generator/src/OperatorFactoryGenerator.php | 1 - generator/src/OperatorGenerator.php | 12 ++- phpcs.xml.dist | 6 ++ src/Builder/Aggregation.php | 41 ++++++++-- src/Builder/Aggregation/AndAggregation.php | 15 ++-- src/Builder/Aggregation/EqAggregation.php | 5 ++ src/Builder/Aggregation/FilterAggregation.php | 24 +++--- src/Builder/Aggregation/GtAggregation.php | 5 ++ src/Builder/Aggregation/GteAggregation.php | 5 ++ src/Builder/Aggregation/LtAggregation.php | 5 ++ src/Builder/Aggregation/NeAggregation.php | 5 ++ src/Builder/Aggregation/SumAggregation.php | 4 + .../{BuilderCodec.php => BuilderEncoder.php} | 22 ++--- src/Builder/Expression.php | 80 +++++++++++++++++-- src/Builder/Expression/ArrayFieldPath.php | 19 +++++ src/Builder/Expression/BoolFieldPath.php | 19 +++++ src/Builder/Expression/DateFieldPath.php | 19 +++++ src/Builder/Expression/DecimalFieldPath.php | 19 +++++ ...Expression.php => ExpressionInterface.php} | 2 +- src/Builder/Expression/ExpressionObject.php | 15 +--- src/Builder/Expression/FieldPath.php | 2 +- src/Builder/Expression/FloatFieldPath.php | 19 +++++ src/Builder/Expression/IntFieldPath.php | 19 +++++ src/Builder/Expression/Literal.php | 2 +- src/Builder/Expression/NullFieldPath.php | 19 +++++ src/Builder/Expression/NumberFieldPath.php | 19 +++++ src/Builder/Expression/ObjectFieldPath.php | 19 +++++ src/Builder/Expression/Operator.php | 7 +- src/Builder/Expression/ResolvesToArray.php | 7 +- src/Builder/Expression/ResolvesToBool.php | 2 +- src/Builder/Expression/ResolvesToDate.php | 2 +- src/Builder/Expression/ResolvesToDecimal.php | 5 +- src/Builder/Expression/ResolvesToFloat.php | 4 +- src/Builder/Expression/ResolvesToInt.php | 4 +- src/Builder/Expression/ResolvesToNull.php | 2 +- src/Builder/Expression/ResolvesToNumber.php | 7 +- src/Builder/Expression/ResolvesToObject.php | 7 +- src/Builder/Expression/ResolvesToString.php | 2 +- src/Builder/Expression/StringFieldPath.php | 19 +++++ src/Builder/Expression/Variable.php | 2 +- src/Builder/Pipeline.php | 4 +- src/Builder/Query.php | 21 ++++- src/Builder/Query/AndQuery.php | 11 ++- src/Builder/Query/ExprQuery.php | 7 +- src/Builder/Query/GtQuery.php | 4 + src/Builder/Query/GteQuery.php | 4 + src/Builder/Query/LtQuery.php | 4 + src/Builder/Query/OrQuery.php | 15 ++-- src/Builder/Stage.php | 21 +++-- src/Builder/Stage/GroupStage.php | 8 +- src/Builder/Stage/LimitStage.php | 6 +- src/Builder/Stage/MatchStage.php | 7 +- src/Builder/Stage/ProjectStage.php | 6 +- src/Builder/Stage/SortStage.php | 6 +- .../Stage/{Stage.php => StageInterface.php} | 2 +- ...iderCodecTest.php => BuilderCodecTest.php} | 9 ++- 65 files changed, 591 insertions(+), 180 deletions(-) create mode 100644 generator/src/ExpressionFactoryGenerator.php rename src/Builder/{BuilderCodec.php => BuilderEncoder.php} (91%) create mode 100644 src/Builder/Expression/ArrayFieldPath.php create mode 100644 src/Builder/Expression/BoolFieldPath.php create mode 100644 src/Builder/Expression/DateFieldPath.php create mode 100644 src/Builder/Expression/DecimalFieldPath.php rename src/Builder/Expression/{Expression.php => ExpressionInterface.php} (84%) create mode 100644 src/Builder/Expression/FloatFieldPath.php create mode 100644 src/Builder/Expression/IntFieldPath.php create mode 100644 src/Builder/Expression/NullFieldPath.php create mode 100644 src/Builder/Expression/NumberFieldPath.php create mode 100644 src/Builder/Expression/ObjectFieldPath.php create mode 100644 src/Builder/Expression/StringFieldPath.php rename src/Builder/Stage/{Stage.php => StageInterface.php} (64%) rename tests/Builder/{BuiderCodecTest.php => BuilderCodecTest.php} (93%) diff --git a/generator/README.md b/generator/README.md index f77d20cfe..2d8175598 100644 --- a/generator/README.md +++ b/generator/README.md @@ -12,7 +12,6 @@ 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: `bin/console generate` -1. To apply the coding standards of the project, run `vendor/bin/phpcbf` from the root of the repository: `cd .. && vendor/bin/phpcbf` ## Configuration diff --git a/generator/config/expressions.php b/generator/config/expressions.php index f0c65f94f..0b448fec9 100644 --- a/generator/config/expressions.php +++ b/generator/config/expressions.php @@ -8,74 +8,97 @@ 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 [ - Expression::class => [ + // Use Interface suffix to avoid confusion with MongoDB\Builder\Expression factory class + ExpressionInterface::class => [ 'types' => ['mixed'], ], // @todo if replaced by a string, it must start with $ FieldPath::class => [ 'class' => true, - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['string'], ], // @todo if replaced by a string, it must start with $$ Variable::class => [ 'class' => true, - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['string'], ], Literal::class => [ 'class' => true, - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['mixed'], ], + // @todo check for use-case ExpressionObject::class => [ - 'class' => true, - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['array', stdClass::class, BSON\Document::class, BSON\Serializable::class], ], + // @todo check for use-case Operator::class => [ - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['array', stdClass::class, BSON\Document::class, BSON\Serializable::class], ], ResolvesToArray::class => [ - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['list', BSONArray::class, BSON\PackedArray::class], ], + ArrayFieldPath::class => typeFieldPath(ResolvesToArray::class), ResolvesToBool::class => [ - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['bool'], ], + BoolFieldPath::class => typeFieldPath(ResolvesToBool::class), ResolvesToDate::class => [ - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['DateTimeInterface', 'UTCDateTime'], ], + DateFieldPath::class => typeFieldPath(ResolvesToDate::class), ResolvesToObject::class => [ - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['array', 'object', BSON\Document::class, BSON\Serializable::class], ], + ObjectFieldPath::class => typeFieldPath(ResolvesToObject::class), ResolvesToNull::class => [ - 'implements' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['null'], ], + NullFieldPath::class => typeFieldPath(ResolvesToNull::class), ResolvesToNumber::class => [ - 'implements' => [Expression::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' => [Expression::class], + 'implements' => [ExpressionInterface::class], 'types' => ['string'], ], + StringFieldPath::class => typeFieldPath(ResolvesToString::class), ]; diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index b38a76bb7..86c9e7da9 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -48,10 +48,9 @@ - name: filter encode: object type: resolvesToArray - usesNamedArgs: true args: - name: input - type: [resolvesToArray, fieldPath] + type: resolvesToArray - name: cond type: resolvesToBool - name: as diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index 2d0d29e5e..743c585a6 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -6,6 +6,7 @@ use MongoDB\CodeGenerator\Definition\ExpressionDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\ExpressionClassGenerator; +use MongoDB\CodeGenerator\ExpressionFactoryGenerator; use MongoDB\CodeGenerator\OperatorGenerator; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -52,12 +53,16 @@ private function generateExpressionClasses(OutputInterface $output): void $config = require $this->configDir . '/expressions.php'; assert(is_array($config)); + $definitions = []; $generator = new ExpressionClassGenerator($this->rootDir); foreach ($config as $name => $def) { assert(is_array($def)); - $def = new ExpressionDefinition($name, ...$def); + $definitions[$name] = $def = new ExpressionDefinition($name, ...$def); $generator->generate($def); } + + $generator = new ExpressionFactoryGenerator($this->rootDir); + $generator->generate($definitions); } private function generate(array $def, OutputInterface $output): void diff --git a/generator/src/Definition/ExpressionDefinition.php b/generator/src/Definition/ExpressionDefinition.php index c629919ec..8dba36468 100644 --- a/generator/src/Definition/ExpressionDefinition.php +++ b/generator/src/Definition/ExpressionDefinition.php @@ -3,6 +3,8 @@ namespace MongoDB\CodeGenerator\Definition; +use InvalidArgumentException; + final readonly class ExpressionDefinition { public function __construct( @@ -10,8 +12,12 @@ public function __construct( /** @var list */ public array $types, public bool $class = false, + public ?string $extends = null, /** @var list */ public array $implements = [], ) { + if ($extends && ! $class) { + throw new InvalidArgumentException('Cannot specify "extends" when "class" is not true'); + } } } diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index 5bf42a478..76061f07b 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -19,7 +19,6 @@ public function __construct( public string $name, public ?string $encode = null, public ?string $type = null, - public bool $usesNamedArgs = false, array $args = [], ) { if ($encode === null && count($args) !== 1) { diff --git a/generator/src/ExpressionClassGenerator.php b/generator/src/ExpressionClassGenerator.php index f8c806f98..a045909ab 100644 --- a/generator/src/ExpressionClassGenerator.php +++ b/generator/src/ExpressionClassGenerator.php @@ -13,7 +13,7 @@ use function str_contains; /** - * Generates a value object class for stages and operators. + * Generates a value object class for expressions */ class ExpressionClassGenerator extends AbstractGenerator { @@ -22,10 +22,6 @@ public function generate(ExpressionDefinition $definition): void $this->writeFile($this->createClassOrInterface($definition)); } - /** - * @param string $className - * @return object{$class:ClassType|InterfaceType, $use: - */ public function createClassOrInterface(ExpressionDefinition $definition): PhpNamespace { [$namespace, $className] = $this->splitNamespaceAndClassName($definition->name); @@ -45,7 +41,9 @@ public function createClassOrInterface(ExpressionDefinition $definition): PhpNam if ($definition->class) { $class = $namespace->addClass($className); $class->setImplements($definition->implements); - $class->setFinal(); + if ($definition->extends) { + $class->setExtends($definition->extends); + } // Replace with promoted property in PHP 8 $propertyType = Type::union(...$types); @@ -61,6 +59,7 @@ public function createClassOrInterface(ExpressionDefinition $definition): PhpNam $class->setExtends($definition->implements); } + // @todo add namespace use for types classes & interfaces $types = array_map( function (string $type): string|Literal { if (str_contains($type, '\\')) { diff --git a/generator/src/ExpressionFactoryGenerator.php b/generator/src/ExpressionFactoryGenerator.php new file mode 100644 index 000000000..e208f424f --- /dev/null +++ b/generator/src/ExpressionFactoryGenerator.php @@ -0,0 +1,51 @@ + $definitions */ + public function generate(array $expressions): void + { + $this->writeFile($this->createFactoryClass($expressions)); + } + + /** @param array $expressions */ + private function createFactoryClass(array $expressions): PhpNamespace + { + $namespace = new PhpNamespace('MongoDB\\Builder'); + $class = $namespace->addClass('Expression'); + $class->setFinal(); + + // Pedantry requires methods to be ordered alphabetically + usort($expressions, fn (ExpressionDefinition $a, ExpressionDefinition $b) => $a->name <=> $b->name); + + foreach ($expressions as $expression) { + if (! $expression->class) { + continue; + } + + $namespace->addUse($expression->name); + $expressionShortClassName = $this->splitNamespaceAndClassName($expression->name)[1]; + + $method = $class->addMethod(lcfirst($expressionShortClassName)); + $method->setStatic(); + $method->addParameter('expression')->setType('string'); + $method->addBody('return new ' . $expressionShortClassName . '($expression);'); + $method->setReturnType($expression->name); + } + + // Pedantry requires private methods to be at the end + $class->addMethod('__construct')->setPrivate() + ->setComment('This class cannot be instantiated.'); + + return $namespace; + } +} diff --git a/generator/src/OperatorClassGenerator.php b/generator/src/OperatorClassGenerator.php index 375fab20c..30d98685a 100644 --- a/generator/src/OperatorClassGenerator.php +++ b/generator/src/OperatorClassGenerator.php @@ -3,7 +3,7 @@ namespace MongoDB\CodeGenerator; -use MongoDB\Builder\Stage\Stage; +use MongoDB\Builder\Stage\StageInterface; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; use Nette\PhpGenerator\PhpNamespace; @@ -27,10 +27,17 @@ public function generate(GeneratorDefinition $definition): void public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator): PhpNamespace { $namespace = new PhpNamespace($definition->namespace); + + $interfaces = $this->getInterfaces($operator); + foreach ($interfaces as $interface) { + $namespace->addUse($interface); + } + $class = $namespace->addClass($this->getOperatorClassName($definition, $operator)); - $class->setImplements($this->getInterfaces($operator)); + $class->setImplements($interfaces); // Expose operator metadata as constants + // @todo move to encoder class $class->addConstant('NAME', '$' . $operator->name); $class->addConstant('ENCODE', $operator->encode ?? 'single'); @@ -99,7 +106,7 @@ private function getInterfaces(OperatorDefinition $definition): array } if ($definition->type === 'stage') { - return ['\\' . Stage::class]; + return ['\\' . StageInterface::class]; } $interface = $this->getExpressionTypeInterface($definition->type); diff --git a/generator/src/OperatorFactoryGenerator.php b/generator/src/OperatorFactoryGenerator.php index 1c989ac80..dc2d0bb39 100644 --- a/generator/src/OperatorFactoryGenerator.php +++ b/generator/src/OperatorFactoryGenerator.php @@ -12,7 +12,6 @@ use function str_replace; use function usort; -/** @internal */ final class OperatorFactoryGenerator extends OperatorGenerator { public function generate(GeneratorDefinition $definition): void diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index c938331bb..b8577feaa 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -4,7 +4,7 @@ namespace MongoDB\CodeGenerator; use InvalidArgumentException; -use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\CodeGenerator\Definition\ArgumentDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; @@ -46,12 +46,16 @@ final protected function getOperatorClassName(GeneratorDefinition $definition, O return ucfirst($operator->name) . $definition->classNameSuffix; } - /** @return class-string */ + /** @return class-string */ final protected function getExpressionTypeInterface(string $type): string { + if ('expression' === $type) { + return ExpressionInterface::class; + } + $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($type); - if ($interface !== Expression::class && ! is_subclass_of($interface, Expression::class)) { + if (! is_subclass_of($interface, ExpressionInterface::class)) { throw new InvalidArgumentException(sprintf('Invalid expression type "%s".', $type)); } @@ -80,7 +84,7 @@ final protected function generateExpressionTypes(ArgumentDefinition $arg): objec $listCheck = true; $nativeTypes[$key] = 'array'; $docTypes[$key] = 'list'; - $use[] = '\\' . Expression::class; + $use[] = '\\' . ExpressionInterface::class; continue; } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 1a0254f5b..c852eef2d 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -14,10 +14,16 @@ docs/examples examples generator/src + generator/config + generator/bin tests tools rector.php + + src/Builder/(Aggregation|Expression|Query|Stage)\.php + src/Builder/(Aggregation|Expression|Query|Stage)/*\.php + diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php index 893a830ba..ab584ef58 100644 --- a/src/Builder/Aggregation.php +++ b/src/Builder/Aggregation.php @@ -16,8 +16,7 @@ use MongoDB\Builder\Aggregation\LtAggregation; use MongoDB\Builder\Aggregation\NeAggregation; use MongoDB\Builder\Aggregation\SumAggregation; -use MongoDB\Builder\Expression\Expression; -use MongoDB\Builder\Expression\FieldPath; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToArray; use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Expression\ResolvesToInt; @@ -26,50 +25,78 @@ final class Aggregation { + /** + * @param ExpressionInterface|mixed ...$expressions + */ public static function and(mixed ...$expressions): AndAggregation { return new AndAggregation(...$expressions); } + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public static function eq(mixed $expression1, mixed $expression2): EqAggregation { return new EqAggregation($expression1, $expression2); } /** - * @param BSONArray|FieldPath|PackedArray|ResolvesToArray|list|string $input - * @param ResolvesToString|string|null $as - * @param Int64|ResolvesToInt|int|null $limit + * @param BSONArray|PackedArray|ResolvesToArray|list $input + * @param ResolvesToBool|bool $cond + * @param ResolvesToString|null|string $as + * @param Int64|ResolvesToInt|int|null $limit */ public static function filter( - PackedArray|FieldPath|ResolvesToArray|BSONArray|array|string $input, + PackedArray|ResolvesToArray|BSONArray|array $input, ResolvesToBool|bool $cond, ResolvesToString|null|string $as = null, Int64|ResolvesToInt|int|null $limit = null, - ): FilterAggregation { + ): FilterAggregation + { return new FilterAggregation($input, $cond, $as, $limit); } + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public static function gt(mixed $expression1, mixed $expression2): GtAggregation { return new GtAggregation($expression1, $expression2); } + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public static function gte(mixed $expression1, mixed $expression2): GteAggregation { return new GteAggregation($expression1, $expression2); } + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public static function lt(mixed $expression1, mixed $expression2): LtAggregation { return new LtAggregation($expression1, $expression2); } + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public static function ne(mixed $expression1, mixed $expression2): NeAggregation { return new NeAggregation($expression1, $expression2); } + /** + * @param ExpressionInterface|mixed $expression + */ public static function sum(mixed $expression): SumAggregation { return new SumAggregation($expression); diff --git a/src/Builder/Aggregation/AndAggregation.php b/src/Builder/Aggregation/AndAggregation.php index bf9592ce5..54ca6c46c 100644 --- a/src/Builder/Aggregation/AndAggregation.php +++ b/src/Builder/Aggregation/AndAggregation.php @@ -6,25 +6,24 @@ namespace MongoDB\Builder\Aggregation; -use InvalidArgumentException; -use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; -use function count; -use function sprintf; - class AndAggregation implements ResolvesToBool { public const NAME = '$and'; public const ENCODE = 'single'; - /** @param list ...$expressions */ + /** @param list ...$expressions */ public array $expressions; + /** + * @param ExpressionInterface|mixed $expressions + */ public function __construct(mixed ...$expressions) { - if (count($expressions) < 1) { - throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($expressions))); + if (\count($expressions) < 1) { + throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', 1, \count($expressions))); } $this->expressions = $expressions; diff --git a/src/Builder/Aggregation/EqAggregation.php b/src/Builder/Aggregation/EqAggregation.php index 4ca188690..f70757755 100644 --- a/src/Builder/Aggregation/EqAggregation.php +++ b/src/Builder/Aggregation/EqAggregation.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Aggregation; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class EqAggregation implements ResolvesToBool @@ -16,6 +17,10 @@ class EqAggregation implements ResolvesToBool public mixed $expression1; public mixed $expression2; + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public function __construct(mixed $expression1, mixed $expression2) { $this->expression1 = $expression1; diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php index 3524be210..a14925768 100644 --- a/src/Builder/Aggregation/FilterAggregation.php +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -6,46 +6,40 @@ namespace MongoDB\Builder\Aggregation; -use InvalidArgumentException; use MongoDB\BSON\Int64; use MongoDB\BSON\PackedArray; -use MongoDB\Builder\Expression\Expression; -use MongoDB\Builder\Expression\FieldPath; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToArray; use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Expression\ResolvesToInt; use MongoDB\Builder\Expression\ResolvesToString; use MongoDB\Model\BSONArray; -use function array_is_list; -use function is_array; -use function sprintf; - class FilterAggregation implements ResolvesToArray { public const NAME = '$filter'; public const ENCODE = 'object'; - public PackedArray|FieldPath|ResolvesToArray|BSONArray|array|string $input; + public PackedArray|ResolvesToArray|BSONArray|array $input; public ResolvesToBool|bool $cond; public ResolvesToString|null|string $as; public Int64|ResolvesToInt|int|null $limit; /** - * @param BSONArray|FieldPath|PackedArray|ResolvesToArray|list|string $input - * @param ResolvesToString|string|null $as - * @param Int64|ResolvesToInt|int|null $limit + * @param BSONArray|PackedArray|ResolvesToArray|list $input + * @param ResolvesToBool|bool $cond + * @param ResolvesToString|null|string $as + * @param Int64|ResolvesToInt|int|null $limit */ public function __construct( - PackedArray|FieldPath|ResolvesToArray|BSONArray|array|string $input, + PackedArray|ResolvesToArray|BSONArray|array $input, ResolvesToBool|bool $cond, ResolvesToString|null|string $as = null, Int64|ResolvesToInt|int|null $limit = null, ) { - if (is_array($input) && ! array_is_list($input)) { - throw new InvalidArgumentException(sprintf('Expected $input argument to be a list, got an associative array.')); + if (\is_array($input) && ! \array_is_list($input)) { + throw new \InvalidArgumentException(\sprintf('Expected $input argument to be a list, got an associative array.')); } - $this->input = $input; $this->cond = $cond; $this->as = $as; diff --git a/src/Builder/Aggregation/GtAggregation.php b/src/Builder/Aggregation/GtAggregation.php index 5b4a70563..af8b16a00 100644 --- a/src/Builder/Aggregation/GtAggregation.php +++ b/src/Builder/Aggregation/GtAggregation.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Aggregation; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class GtAggregation implements ResolvesToBool @@ -16,6 +17,10 @@ class GtAggregation implements ResolvesToBool public mixed $expression1; public mixed $expression2; + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public function __construct(mixed $expression1, mixed $expression2) { $this->expression1 = $expression1; diff --git a/src/Builder/Aggregation/GteAggregation.php b/src/Builder/Aggregation/GteAggregation.php index 5142eb275..b8933db72 100644 --- a/src/Builder/Aggregation/GteAggregation.php +++ b/src/Builder/Aggregation/GteAggregation.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Aggregation; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class GteAggregation implements ResolvesToBool @@ -16,6 +17,10 @@ class GteAggregation implements ResolvesToBool public mixed $expression1; public mixed $expression2; + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public function __construct(mixed $expression1, mixed $expression2) { $this->expression1 = $expression1; diff --git a/src/Builder/Aggregation/LtAggregation.php b/src/Builder/Aggregation/LtAggregation.php index 45c0d0fa0..a135ed169 100644 --- a/src/Builder/Aggregation/LtAggregation.php +++ b/src/Builder/Aggregation/LtAggregation.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Aggregation; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class LtAggregation implements ResolvesToBool @@ -16,6 +17,10 @@ class LtAggregation implements ResolvesToBool public mixed $expression1; public mixed $expression2; + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public function __construct(mixed $expression1, mixed $expression2) { $this->expression1 = $expression1; diff --git a/src/Builder/Aggregation/NeAggregation.php b/src/Builder/Aggregation/NeAggregation.php index d25524369..c277e8134 100644 --- a/src/Builder/Aggregation/NeAggregation.php +++ b/src/Builder/Aggregation/NeAggregation.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Aggregation; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class NeAggregation implements ResolvesToBool @@ -16,6 +17,10 @@ class NeAggregation implements ResolvesToBool public mixed $expression1; public mixed $expression2; + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ public function __construct(mixed $expression1, mixed $expression2) { $this->expression1 = $expression1; diff --git a/src/Builder/Aggregation/SumAggregation.php b/src/Builder/Aggregation/SumAggregation.php index cc17f006c..2a4e232c1 100644 --- a/src/Builder/Aggregation/SumAggregation.php +++ b/src/Builder/Aggregation/SumAggregation.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Aggregation; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToInt; class SumAggregation implements ResolvesToInt @@ -15,6 +16,9 @@ class SumAggregation implements ResolvesToInt public mixed $expression; + /** + * @param ExpressionInterface|mixed $expression + */ public function __construct(mixed $expression) { $this->expression = $expression; diff --git a/src/Builder/BuilderCodec.php b/src/Builder/BuilderEncoder.php similarity index 91% rename from src/Builder/BuilderCodec.php rename to src/Builder/BuilderEncoder.php index 082354a3a..fdb115183 100644 --- a/src/Builder/BuilderCodec.php +++ b/src/Builder/BuilderEncoder.php @@ -3,16 +3,15 @@ namespace MongoDB\Builder; use LogicException; -use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\FieldPath; use MongoDB\Builder\Expression\Variable; use MongoDB\Builder\Query\OrQuery; use MongoDB\Builder\Stage\GroupStage; use MongoDB\Builder\Stage\ProjectStage; -use MongoDB\Builder\Stage\Stage; -use MongoDB\Codec\Codec; -use MongoDB\Codec\DecodeIfSupported; +use MongoDB\Builder\Stage\StageInterface; use MongoDB\Codec\EncodeIfSupported; +use MongoDB\Codec\Encoder; use MongoDB\Exception\UnsupportedValueException; use stdClass; @@ -21,9 +20,8 @@ use function is_array; use function sprintf; -class BuilderCodec implements Codec +class BuilderEncoder implements Encoder { - use DecodeIfSupported; use EncodeIfSupported; /** The first property is the operator value */ @@ -35,19 +33,9 @@ class BuilderCodec implements Codec /** Properties are encoded as a list of values, names are ignored */ public const ENCODE_AS_ARRAY = 'array'; - public function canDecode($value): false - { - return false; - } - public function canEncode($value): bool { - return $value instanceof Pipeline || $value instanceof Stage || $value instanceof Expression; - } - - public function decode($value) - { - throw UnsupportedValueException::invalidDecodableValue($value); + return $value instanceof Pipeline || $value instanceof StageInterface || $value instanceof ExpressionInterface; } public function encode($value): array|stdClass|string|int|float|bool|null diff --git a/src/Builder/Expression.php b/src/Builder/Expression.php index e32021057..267b1d234 100644 --- a/src/Builder/Expression.php +++ b/src/Builder/Expression.php @@ -1,26 +1,90 @@ expression = $expression; + } +} diff --git a/src/Builder/Expression/BoolFieldPath.php b/src/Builder/Expression/BoolFieldPath.php new file mode 100644 index 000000000..a6a320ad0 --- /dev/null +++ b/src/Builder/Expression/BoolFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/DateFieldPath.php b/src/Builder/Expression/DateFieldPath.php new file mode 100644 index 000000000..dbc7d8c42 --- /dev/null +++ b/src/Builder/Expression/DateFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/DecimalFieldPath.php b/src/Builder/Expression/DecimalFieldPath.php new file mode 100644 index 000000000..75a22506e --- /dev/null +++ b/src/Builder/Expression/DecimalFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/Expression.php b/src/Builder/Expression/ExpressionInterface.php similarity index 84% rename from src/Builder/Expression/Expression.php rename to src/Builder/Expression/ExpressionInterface.php index 665e6cb50..28a6a1c1a 100644 --- a/src/Builder/Expression/Expression.php +++ b/src/Builder/Expression/ExpressionInterface.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -interface Expression +interface ExpressionInterface { public const ACCEPTED_TYPES = ['mixed']; } diff --git a/src/Builder/Expression/ExpressionObject.php b/src/Builder/Expression/ExpressionObject.php index f005a6f46..8573ccdf2 100644 --- a/src/Builder/Expression/ExpressionObject.php +++ b/src/Builder/Expression/ExpressionObject.php @@ -6,18 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\Document; -use MongoDB\BSON\Serializable; -use stdClass; - -final class ExpressionObject implements Expression +interface ExpressionObject extends ExpressionInterface { - public const ACCEPTED_TYPES = ['array', 'stdClass', Document::class, Serializable::class]; - - public array|stdClass|Document|Serializable $expression; - - public function __construct(array|stdClass|Document|Serializable $expression) - { - $this->expression = $expression; - } + public const ACCEPTED_TYPES = ['array', 'stdClass', \MongoDB\BSON\Document::class, \MongoDB\BSON\Serializable::class]; } diff --git a/src/Builder/Expression/FieldPath.php b/src/Builder/Expression/FieldPath.php index 0b55db444..5285f2e19 100644 --- a/src/Builder/Expression/FieldPath.php +++ b/src/Builder/Expression/FieldPath.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -final class FieldPath implements Expression +class FieldPath implements ExpressionInterface { public const ACCEPTED_TYPES = ['string']; diff --git a/src/Builder/Expression/FloatFieldPath.php b/src/Builder/Expression/FloatFieldPath.php new file mode 100644 index 000000000..bb933f37d --- /dev/null +++ b/src/Builder/Expression/FloatFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/IntFieldPath.php b/src/Builder/Expression/IntFieldPath.php new file mode 100644 index 000000000..c77f51e05 --- /dev/null +++ b/src/Builder/Expression/IntFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/Literal.php b/src/Builder/Expression/Literal.php index ac35f322a..1077c333a 100644 --- a/src/Builder/Expression/Literal.php +++ b/src/Builder/Expression/Literal.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -final class Literal implements Expression +class Literal implements ExpressionInterface { public const ACCEPTED_TYPES = ['mixed']; diff --git a/src/Builder/Expression/NullFieldPath.php b/src/Builder/Expression/NullFieldPath.php new file mode 100644 index 000000000..69554ffac --- /dev/null +++ b/src/Builder/Expression/NullFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/NumberFieldPath.php b/src/Builder/Expression/NumberFieldPath.php new file mode 100644 index 000000000..2efdb35f4 --- /dev/null +++ b/src/Builder/Expression/NumberFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/ObjectFieldPath.php b/src/Builder/Expression/ObjectFieldPath.php new file mode 100644 index 000000000..b08cfe1a2 --- /dev/null +++ b/src/Builder/Expression/ObjectFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/Operator.php b/src/Builder/Expression/Operator.php index 4f8eb08bb..a82e783a7 100644 --- a/src/Builder/Expression/Operator.php +++ b/src/Builder/Expression/Operator.php @@ -6,10 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\Document; -use MongoDB\BSON\Serializable; - -interface Operator extends Expression +interface Operator extends ExpressionInterface { - public const ACCEPTED_TYPES = ['array', 'stdClass', Document::class, Serializable::class]; + public const ACCEPTED_TYPES = ['array', 'stdClass', \MongoDB\BSON\Document::class, \MongoDB\BSON\Serializable::class]; } diff --git a/src/Builder/Expression/ResolvesToArray.php b/src/Builder/Expression/ResolvesToArray.php index a5d625e62..d23b48135 100644 --- a/src/Builder/Expression/ResolvesToArray.php +++ b/src/Builder/Expression/ResolvesToArray.php @@ -6,10 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\PackedArray; -use MongoDB\Model\BSONArray; - -interface ResolvesToArray extends Expression +interface ResolvesToArray extends ExpressionInterface { - public const ACCEPTED_TYPES = ['list', BSONArray::class, PackedArray::class]; + public const ACCEPTED_TYPES = ['list', \MongoDB\Model\BSONArray::class, \MongoDB\BSON\PackedArray::class]; } diff --git a/src/Builder/Expression/ResolvesToBool.php b/src/Builder/Expression/ResolvesToBool.php index cf141d961..650db5ba8 100644 --- a/src/Builder/Expression/ResolvesToBool.php +++ b/src/Builder/Expression/ResolvesToBool.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -interface ResolvesToBool extends Expression +interface ResolvesToBool extends ExpressionInterface { public const ACCEPTED_TYPES = ['bool']; } diff --git a/src/Builder/Expression/ResolvesToDate.php b/src/Builder/Expression/ResolvesToDate.php index f9330e673..1b11cd1dc 100644 --- a/src/Builder/Expression/ResolvesToDate.php +++ b/src/Builder/Expression/ResolvesToDate.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -interface ResolvesToDate extends Expression +interface ResolvesToDate extends ExpressionInterface { public const ACCEPTED_TYPES = ['DateTimeInterface', 'UTCDateTime']; } diff --git a/src/Builder/Expression/ResolvesToDecimal.php b/src/Builder/Expression/ResolvesToDecimal.php index 716b28ea1..376594e1e 100644 --- a/src/Builder/Expression/ResolvesToDecimal.php +++ b/src/Builder/Expression/ResolvesToDecimal.php @@ -6,10 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\Decimal128; -use MongoDB\BSON\Int64; - interface ResolvesToDecimal extends ResolvesToNumber { - public const ACCEPTED_TYPES = ['int', 'float', Int64::class, Decimal128::class]; + public const ACCEPTED_TYPES = ['int', 'float', \MongoDB\BSON\Int64::class, \MongoDB\BSON\Decimal128::class]; } diff --git a/src/Builder/Expression/ResolvesToFloat.php b/src/Builder/Expression/ResolvesToFloat.php index aa2cc7db8..81b0a3e98 100644 --- a/src/Builder/Expression/ResolvesToFloat.php +++ b/src/Builder/Expression/ResolvesToFloat.php @@ -6,9 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\Int64; - interface ResolvesToFloat extends ResolvesToNumber { - public const ACCEPTED_TYPES = ['int', 'float', Int64::class]; + public const ACCEPTED_TYPES = ['int', 'float', \MongoDB\BSON\Int64::class]; } diff --git a/src/Builder/Expression/ResolvesToInt.php b/src/Builder/Expression/ResolvesToInt.php index 11d020ed0..ce36a61bb 100644 --- a/src/Builder/Expression/ResolvesToInt.php +++ b/src/Builder/Expression/ResolvesToInt.php @@ -6,9 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\Int64; - interface ResolvesToInt extends ResolvesToNumber { - public const ACCEPTED_TYPES = ['int', Int64::class]; + public const ACCEPTED_TYPES = ['int', \MongoDB\BSON\Int64::class]; } diff --git a/src/Builder/Expression/ResolvesToNull.php b/src/Builder/Expression/ResolvesToNull.php index 3a5f4e9a7..ff8cd9061 100644 --- a/src/Builder/Expression/ResolvesToNull.php +++ b/src/Builder/Expression/ResolvesToNull.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -interface ResolvesToNull extends Expression +interface ResolvesToNull extends ExpressionInterface { public const ACCEPTED_TYPES = ['null']; } diff --git a/src/Builder/Expression/ResolvesToNumber.php b/src/Builder/Expression/ResolvesToNumber.php index bb717b6e6..71e2ba187 100644 --- a/src/Builder/Expression/ResolvesToNumber.php +++ b/src/Builder/Expression/ResolvesToNumber.php @@ -6,10 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\Decimal128; -use MongoDB\BSON\Int64; - -interface ResolvesToNumber extends Expression +interface ResolvesToNumber extends ExpressionInterface { - public const ACCEPTED_TYPES = ['int', 'float', Int64::class, Decimal128::class]; + public const ACCEPTED_TYPES = ['int', 'float', \MongoDB\BSON\Int64::class, \MongoDB\BSON\Decimal128::class]; } diff --git a/src/Builder/Expression/ResolvesToObject.php b/src/Builder/Expression/ResolvesToObject.php index 9fa07f2c3..67a19fd1e 100644 --- a/src/Builder/Expression/ResolvesToObject.php +++ b/src/Builder/Expression/ResolvesToObject.php @@ -6,10 +6,7 @@ namespace MongoDB\Builder\Expression; -use MongoDB\BSON\Document; -use MongoDB\BSON\Serializable; - -interface ResolvesToObject extends Expression +interface ResolvesToObject extends ExpressionInterface { - public const ACCEPTED_TYPES = ['array', 'object', Document::class, Serializable::class]; + public const ACCEPTED_TYPES = ['array', 'object', \MongoDB\BSON\Document::class, \MongoDB\BSON\Serializable::class]; } diff --git a/src/Builder/Expression/ResolvesToString.php b/src/Builder/Expression/ResolvesToString.php index 4d80a3739..f37df9d1a 100644 --- a/src/Builder/Expression/ResolvesToString.php +++ b/src/Builder/Expression/ResolvesToString.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -interface ResolvesToString extends Expression +interface ResolvesToString extends ExpressionInterface { public const ACCEPTED_TYPES = ['string']; } diff --git a/src/Builder/Expression/StringFieldPath.php b/src/Builder/Expression/StringFieldPath.php new file mode 100644 index 000000000..ad4aeb80f --- /dev/null +++ b/src/Builder/Expression/StringFieldPath.php @@ -0,0 +1,19 @@ +expression = $expression; + } +} diff --git a/src/Builder/Expression/Variable.php b/src/Builder/Expression/Variable.php index b23781c7f..cb2207f08 100644 --- a/src/Builder/Expression/Variable.php +++ b/src/Builder/Expression/Variable.php @@ -6,7 +6,7 @@ namespace MongoDB\Builder\Expression; -final class Variable implements Expression +class Variable implements ExpressionInterface { public const ACCEPTED_TYPES = ['string']; diff --git a/src/Builder/Pipeline.php b/src/Builder/Pipeline.php index 6eef85899..039ff5f8b 100644 --- a/src/Builder/Pipeline.php +++ b/src/Builder/Pipeline.php @@ -2,7 +2,7 @@ namespace MongoDB\Builder; -use MongoDB\Builder\Stage\Stage; +use MongoDB\Builder\Stage\StageInterface; use MongoDB\Exception\InvalidArgumentException; use function array_is_list; @@ -12,7 +12,7 @@ class Pipeline public array $stages; public function __construct( - Stage ...$stages + StageInterface ...$stages ) { if (! array_is_list($stages)) { throw new InvalidArgumentException('Expected $stages argument to be a list, got an associative array.'); diff --git a/src/Builder/Query.php b/src/Builder/Query.php index 7d0720c16..4d829cd9d 100644 --- a/src/Builder/Query.php +++ b/src/Builder/Query.php @@ -6,41 +6,60 @@ namespace MongoDB\Builder; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Query\AndQuery; use MongoDB\Builder\Query\ExprQuery; -use MongoDB\Builder\Query\GteQuery; use MongoDB\Builder\Query\GtQuery; +use MongoDB\Builder\Query\GteQuery; use MongoDB\Builder\Query\LtQuery; use MongoDB\Builder\Query\OrQuery; final class Query { + /** + * @param ResolvesToBool|bool ...$query + */ public static function and(ResolvesToBool|bool ...$query): AndQuery { return new AndQuery(...$query); } + /** + * @param ExpressionInterface|mixed $expression + */ public static function expr(mixed $expression): ExprQuery { return new ExprQuery($expression); } + /** + * @param ExpressionInterface|mixed $value + */ public static function gt(mixed $value): GtQuery { return new GtQuery($value); } + /** + * @param ExpressionInterface|mixed $value + */ public static function gte(mixed $value): GteQuery { return new GteQuery($value); } + /** + * @param ExpressionInterface|mixed $value + */ public static function lt(mixed $value): LtQuery { return new LtQuery($value); } + /** + * @param ExpressionInterface|mixed ...$query + */ public static function or(mixed ...$query): OrQuery { return new OrQuery(...$query); diff --git a/src/Builder/Query/AndQuery.php b/src/Builder/Query/AndQuery.php index 2d4337fac..e572f7036 100644 --- a/src/Builder/Query/AndQuery.php +++ b/src/Builder/Query/AndQuery.php @@ -6,12 +6,8 @@ namespace MongoDB\Builder\Query; -use InvalidArgumentException; use MongoDB\Builder\Expression\ResolvesToBool; -use function count; -use function sprintf; - class AndQuery implements ResolvesToBool { public const NAME = '$and'; @@ -20,10 +16,13 @@ class AndQuery implements ResolvesToBool /** @param list ...$query */ public array $query; + /** + * @param ResolvesToBool|bool $query + */ public function __construct(ResolvesToBool|bool ...$query) { - if (count($query) < 1) { - throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($query))); + if (\count($query) < 1) { + throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', 1, \count($query))); } $this->query = $query; diff --git a/src/Builder/Query/ExprQuery.php b/src/Builder/Query/ExprQuery.php index 3b505bd76..810652b9c 100644 --- a/src/Builder/Query/ExprQuery.php +++ b/src/Builder/Query/ExprQuery.php @@ -6,15 +6,18 @@ namespace MongoDB\Builder\Query; -use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ExpressionInterface; -class ExprQuery implements Expression +class ExprQuery implements ExpressionInterface { public const NAME = '$expr'; public const ENCODE = 'single'; public mixed $expression; + /** + * @param ExpressionInterface|mixed $expression + */ public function __construct(mixed $expression) { $this->expression = $expression; diff --git a/src/Builder/Query/GtQuery.php b/src/Builder/Query/GtQuery.php index 0a5e5b702..58c558aed 100644 --- a/src/Builder/Query/GtQuery.php +++ b/src/Builder/Query/GtQuery.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Query; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class GtQuery implements ResolvesToBool @@ -15,6 +16,9 @@ class GtQuery implements ResolvesToBool public mixed $value; + /** + * @param ExpressionInterface|mixed $value + */ public function __construct(mixed $value) { $this->value = $value; diff --git a/src/Builder/Query/GteQuery.php b/src/Builder/Query/GteQuery.php index 124146638..130a56bd4 100644 --- a/src/Builder/Query/GteQuery.php +++ b/src/Builder/Query/GteQuery.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Query; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class GteQuery implements ResolvesToBool @@ -15,6 +16,9 @@ class GteQuery implements ResolvesToBool public mixed $value; + /** + * @param ExpressionInterface|mixed $value + */ public function __construct(mixed $value) { $this->value = $value; diff --git a/src/Builder/Query/LtQuery.php b/src/Builder/Query/LtQuery.php index a8630bea7..621fd7d26 100644 --- a/src/Builder/Query/LtQuery.php +++ b/src/Builder/Query/LtQuery.php @@ -6,6 +6,7 @@ namespace MongoDB\Builder\Query; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; class LtQuery implements ResolvesToBool @@ -15,6 +16,9 @@ class LtQuery implements ResolvesToBool public mixed $value; + /** + * @param ExpressionInterface|mixed $value + */ public function __construct(mixed $value) { $this->value = $value; diff --git a/src/Builder/Query/OrQuery.php b/src/Builder/Query/OrQuery.php index 49434277d..5149b7cbc 100644 --- a/src/Builder/Query/OrQuery.php +++ b/src/Builder/Query/OrQuery.php @@ -6,25 +6,24 @@ namespace MongoDB\Builder\Query; -use InvalidArgumentException; -use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToBool; -use function count; -use function sprintf; - class OrQuery implements ResolvesToBool { public const NAME = '$or'; public const ENCODE = 'single'; - /** @param list ...$query */ + /** @param list ...$query */ public array $query; + /** + * @param ExpressionInterface|mixed $query + */ public function __construct(mixed ...$query) { - if (count($query) < 1) { - throw new InvalidArgumentException(sprintf('Expected at least %d values, got %d.', 1, count($query))); + if (\count($query) < 1) { + throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', 1, \count($query))); } $this->query = $query; diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index 857ef3f45..02d97fe02 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -9,7 +9,7 @@ use MongoDB\BSON\Document; use MongoDB\BSON\Int64; use MongoDB\BSON\Serializable; -use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToInt; use MongoDB\Builder\Expression\ResolvesToObject; use MongoDB\Builder\Stage\GroupStage; @@ -21,32 +21,41 @@ final class Stage { /** - * @param Expression|mixed|null $_id - * @param Document|ResolvesToObject|Serializable|array|object|null $fields + * @param ExpressionInterface|mixed|null $_id + * @param Document|ResolvesToObject|Serializable|array|null|object $fields */ public static function group(mixed $_id = null, array|null|object $fields = null): GroupStage { return new GroupStage($_id, $fields); } - /** @param Int64|ResolvesToInt|int $limit */ + /** + * @param Int64|ResolvesToInt|int $limit + */ public static function limit(Int64|ResolvesToInt|int $limit): LimitStage { return new LimitStage($limit); } + /** + * @param ExpressionInterface|mixed $query + */ public static function match(mixed $query): MatchStage { return new MatchStage($query); } - /** @param Document|ResolvesToObject|Serializable|array|object $specifications */ + /** + * @param Document|ResolvesToObject|Serializable|array|object $specifications + */ public static function project(array|object $specifications): ProjectStage { return new ProjectStage($specifications); } - /** @param Document|ResolvesToObject|Serializable|array|object $sortSpecification */ + /** + * @param Document|ResolvesToObject|Serializable|array|object $sortSpecification + */ public static function sort(array|object $sortSpecification): SortStage { return new SortStage($sortSpecification); diff --git a/src/Builder/Stage/GroupStage.php b/src/Builder/Stage/GroupStage.php index f085736bd..4bb9d32d1 100644 --- a/src/Builder/Stage/GroupStage.php +++ b/src/Builder/Stage/GroupStage.php @@ -8,10 +8,10 @@ use MongoDB\BSON\Document; use MongoDB\BSON\Serializable; -use MongoDB\Builder\Expression\Expression; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToObject; -class GroupStage implements Stage +class GroupStage implements StageInterface { public const NAME = '$group'; public const ENCODE = 'object'; @@ -20,8 +20,8 @@ class GroupStage implements Stage public array|null|object $fields; /** - * @param Expression|mixed|null $_id - * @param Document|ResolvesToObject|Serializable|array|object|null $fields + * @param ExpressionInterface|mixed|null $_id + * @param Document|ResolvesToObject|Serializable|array|null|object $fields */ public function __construct(mixed $_id = null, array|null|object $fields = null) { diff --git a/src/Builder/Stage/LimitStage.php b/src/Builder/Stage/LimitStage.php index 72184a8fa..e8ed074f8 100644 --- a/src/Builder/Stage/LimitStage.php +++ b/src/Builder/Stage/LimitStage.php @@ -9,14 +9,16 @@ use MongoDB\BSON\Int64; use MongoDB\Builder\Expression\ResolvesToInt; -class LimitStage implements Stage +class LimitStage implements StageInterface { public const NAME = '$limit'; public const ENCODE = 'single'; public Int64|ResolvesToInt|int $limit; - /** @param Int64|ResolvesToInt|int $limit */ + /** + * @param Int64|ResolvesToInt|int $limit + */ public function __construct(Int64|ResolvesToInt|int $limit) { $this->limit = $limit; diff --git a/src/Builder/Stage/MatchStage.php b/src/Builder/Stage/MatchStage.php index e2026c102..b17041c11 100644 --- a/src/Builder/Stage/MatchStage.php +++ b/src/Builder/Stage/MatchStage.php @@ -6,13 +6,18 @@ namespace MongoDB\Builder\Stage; -class MatchStage implements Stage +use MongoDB\Builder\Expression\ExpressionInterface; + +class MatchStage implements StageInterface { public const NAME = '$match'; public const ENCODE = 'single'; public mixed $query; + /** + * @param ExpressionInterface|mixed $query + */ public function __construct(mixed $query) { $this->query = $query; diff --git a/src/Builder/Stage/ProjectStage.php b/src/Builder/Stage/ProjectStage.php index 305f481c1..d51d8efe6 100644 --- a/src/Builder/Stage/ProjectStage.php +++ b/src/Builder/Stage/ProjectStage.php @@ -10,14 +10,16 @@ use MongoDB\BSON\Serializable; use MongoDB\Builder\Expression\ResolvesToObject; -class ProjectStage implements Stage +class ProjectStage implements StageInterface { public const NAME = '$project'; public const ENCODE = 'single'; public array|object $specifications; - /** @param Document|ResolvesToObject|Serializable|array|object $specifications */ + /** + * @param Document|ResolvesToObject|Serializable|array|object $specifications + */ public function __construct(array|object $specifications) { $this->specifications = $specifications; diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php index 29e72fd73..1448debec 100644 --- a/src/Builder/Stage/SortStage.php +++ b/src/Builder/Stage/SortStage.php @@ -10,14 +10,16 @@ use MongoDB\BSON\Serializable; use MongoDB\Builder\Expression\ResolvesToObject; -class SortStage implements Stage +class SortStage implements StageInterface { public const NAME = '$sort'; public const ENCODE = 'single'; public array|object $sortSpecification; - /** @param Document|ResolvesToObject|Serializable|array|object $sortSpecification */ + /** + * @param Document|ResolvesToObject|Serializable|array|object $sortSpecification + */ public function __construct(array|object $sortSpecification) { $this->sortSpecification = $sortSpecification; diff --git a/src/Builder/Stage/Stage.php b/src/Builder/Stage/StageInterface.php similarity index 64% rename from src/Builder/Stage/Stage.php rename to src/Builder/Stage/StageInterface.php index 7893b937a..2e7524f26 100644 --- a/src/Builder/Stage/Stage.php +++ b/src/Builder/Stage/StageInterface.php @@ -2,6 +2,6 @@ namespace MongoDB\Builder\Stage; -interface Stage +interface StageInterface { } diff --git a/tests/Builder/BuiderCodecTest.php b/tests/Builder/BuilderCodecTest.php similarity index 93% rename from tests/Builder/BuiderCodecTest.php rename to tests/Builder/BuilderCodecTest.php index cb3f9ead3..2271e4edb 100644 --- a/tests/Builder/BuiderCodecTest.php +++ b/tests/Builder/BuilderCodecTest.php @@ -4,7 +4,7 @@ use Generator; use MongoDB\Builder\Aggregation; -use MongoDB\Builder\BuilderCodec; +use MongoDB\Builder\BuilderEncoder; use MongoDB\Builder\Expression; use MongoDB\Builder\Pipeline; use MongoDB\Builder\Query; @@ -13,7 +13,7 @@ use function array_merge; -class BuiderCodecTest extends TestCase +class BuilderCodecTest extends TestCase { public function testPipeline(): void { @@ -72,7 +72,7 @@ public function testAggregationFilter($limit, $expectedLimit): void $pipeline = new Pipeline( Stage::project([ 'items' => Aggregation::filter( - input: Expression::fieldPath('items'), + input: Expression::arrayFieldPath('items'), cond: Aggregation::gte(Expression::variable('item.price'), 100), as: 'item', limit: $limit, @@ -112,9 +112,10 @@ public static function provideAggregationFilterLimit(): Generator private static function assertSamePipeline(array $expected, Pipeline $pipeline): void { - $codec = new BuilderCodec(); + $codec = new BuilderEncoder(); $actual = $codec->encode($pipeline); + // @todo walk in array to cast associative array to an object self::assertEquals($expected, $actual); } } From c1e2cb42463dc0fe81dab99327353bfb5ea01a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 28 Sep 2023 20:49:34 +0200 Subject: [PATCH 14/31] Pedantry --- src/Builder/BuilderEncoder.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index fdb115183..985836dee 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -124,17 +124,6 @@ public function encode($value): array|stdClass|string|int|float|bool|null throw new LogicException(sprintf('Class "%s" does not have a valid ENCODE constant.', $value::class)); } - private function encodeAsSingle($value): stdClass - { - $result = []; - foreach ($value as $val) { - $result = $this->encodeIfSupported($val); - break; - } - - return (object) [$value::NAME => $result]; - } - private function encodeAsArray($value): stdClass { $result = []; @@ -157,4 +146,15 @@ private function encodeAsObject($value): stdClass return (object) [$value::NAME => $result]; } + + private function encodeAsSingle($value): stdClass + { + $result = []; + foreach ($value as $val) { + $result = $this->encodeIfSupported($val); + break; + } + + return (object) [$value::NAME => $result]; + } } From 3fbb7daa3b60eb48039cfedcc1f338da46c9c0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 28 Sep 2023 21:34:33 +0200 Subject: [PATCH 15/31] Simplify expected pipeline in tests by converting assoc array to objects --- tests/Builder/BuilderCodecTest.php | 52 +++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/tests/Builder/BuilderCodecTest.php b/tests/Builder/BuilderCodecTest.php index 2271e4edb..d129d5495 100644 --- a/tests/Builder/BuilderCodecTest.php +++ b/tests/Builder/BuilderCodecTest.php @@ -11,7 +11,10 @@ use MongoDB\Builder\Stage; use MongoDB\Tests\TestCase; +use function array_is_list; use function array_merge; +use function array_walk; +use function is_array; class BuilderCodecTest extends TestCase { @@ -24,9 +27,9 @@ public function testPipeline(): void ); $expected = [ - (object) ['$match' => (object) ['$eq' => ['$foo', 1]]], - (object) ['$match' => (object) ['$ne' => ['$foo', 2]]], - (object) ['$limit' => 1], + ['$match' => ['$eq' => ['$foo', 1]]], + ['$match' => ['$ne' => ['$foo', 2]]], + ['$limit' => 1], ]; $this->assertSamePipeline($expected, $pipeline); @@ -44,18 +47,18 @@ public function testPerformCount(): void ); $expected = [ - (object) [ - '$match' => (object) [ + [ + '$match' => [ '$or' => [ - (object) ['score' => (object) ['$gt' => 70, '$lt' => 90]], - (object) ['views' => (object) ['$gte' => 1000]], + ['score' => ['$gt' => 70, '$lt' => 90]], + ['views' => ['$gte' => 1000]], ], ], ], - (object) [ - '$group' => (object) [ + [ + '$group' => [ '_id' => null, - 'count' => (object) ['$sum' => 1], + 'count' => ['$sum' => 1], ], ], ]; @@ -81,13 +84,13 @@ public function testAggregationFilter($limit, $expectedLimit): void ); $expected = [ - (object) [ - '$project' => (object) [ - 'items' => (object) [ - '$filter' => (object) array_merge([ + [ + '$project' => [ + 'items' => [ + '$filter' => array_merge([ 'input' => '$items', 'as' => 'item', - 'cond' => (object) ['$gte' => ['$$item.price', 100]], + 'cond' => ['$gte' => ['$$item.price', 100]], ], $expectedLimit), ], ], @@ -115,7 +118,24 @@ private static function assertSamePipeline(array $expected, Pipeline $pipeline): $codec = new BuilderEncoder(); $actual = $codec->encode($pipeline); - // @todo walk in array to cast associative array to an object + self::objectify($expected); + self::assertEquals($expected, $actual); } + + /** + * Recursively convert associative arrays to objects. + */ + private static function objectify(array &$array): void + { + array_walk($array, function (&$value): void { + if (is_array($value)) { + self::objectify($value); + + if (! array_is_list($value)) { + $value = (object) $value; + } + } + }); + } } From eed8ea518d86f15c84b5bf4eff88c63c3f6ac700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 28 Sep 2023 22:14:35 +0200 Subject: [PATCH 16/31] Fix CS --- src/Builder/BuilderEncoder.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index 985836dee..5aac3577d 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -33,11 +33,17 @@ class BuilderEncoder implements Encoder /** Properties are encoded as a list of values, names are ignored */ public const ENCODE_AS_ARRAY = 'array'; + /** + * {@inheritdoc} + */ public function canEncode($value): bool { return $value instanceof Pipeline || $value instanceof StageInterface || $value instanceof ExpressionInterface; } + /** + * {@inheritdoc} + */ public function encode($value): array|stdClass|string|int|float|bool|null { if (! $this->canEncode($value)) { @@ -124,7 +130,7 @@ public function encode($value): array|stdClass|string|int|float|bool|null throw new LogicException(sprintf('Class "%s" does not have a valid ENCODE constant.', $value::class)); } - private function encodeAsArray($value): stdClass + private function encodeAsArray(ExpressionInterface|StageInterface $value): stdClass { $result = []; foreach ($value as $val) { @@ -134,7 +140,7 @@ private function encodeAsArray($value): stdClass return (object) [$value::NAME => $result]; } - private function encodeAsObject($value): stdClass + private function encodeAsObject(ExpressionInterface|StageInterface $value): stdClass { $result = new stdClass(); foreach ($value as $key => $val) { @@ -147,7 +153,7 @@ private function encodeAsObject($value): stdClass return (object) [$value::NAME => $result]; } - private function encodeAsSingle($value): stdClass + private function encodeAsSingle(ExpressionInterface|StageInterface $value): stdClass { $result = []; foreach ($value as $val) { From c1373376f7bd83fdebed7a5fec0aaa724bb33daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 28 Sep 2023 22:29:03 +0200 Subject: [PATCH 17/31] Remove useless ACCEPTED_TYPES constant. Pass types config to the generator --- generator/src/Command/GenerateCommand.php | 46 ++++++++++--------- generator/src/ExpressionClassGenerator.php | 17 ------- generator/src/OperatorGenerator.php | 9 ++-- src/Builder/Expression/ArrayFieldPath.php | 2 - src/Builder/Expression/BoolFieldPath.php | 2 - src/Builder/Expression/DateFieldPath.php | 2 - src/Builder/Expression/DecimalFieldPath.php | 2 - .../Expression/ExpressionInterface.php | 1 - src/Builder/Expression/ExpressionObject.php | 1 - src/Builder/Expression/FieldPath.php | 2 - src/Builder/Expression/FloatFieldPath.php | 2 - src/Builder/Expression/IntFieldPath.php | 2 - src/Builder/Expression/Literal.php | 2 - src/Builder/Expression/NullFieldPath.php | 2 - src/Builder/Expression/NumberFieldPath.php | 2 - src/Builder/Expression/ObjectFieldPath.php | 2 - src/Builder/Expression/Operator.php | 1 - src/Builder/Expression/ResolvesToArray.php | 1 - src/Builder/Expression/ResolvesToBool.php | 1 - src/Builder/Expression/ResolvesToDate.php | 1 - src/Builder/Expression/ResolvesToDecimal.php | 1 - src/Builder/Expression/ResolvesToFloat.php | 1 - src/Builder/Expression/ResolvesToInt.php | 1 - src/Builder/Expression/ResolvesToNull.php | 1 - src/Builder/Expression/ResolvesToNumber.php | 1 - src/Builder/Expression/ResolvesToObject.php | 1 - src/Builder/Expression/ResolvesToString.php | 1 - src/Builder/Expression/StringFieldPath.php | 2 - src/Builder/Expression/Variable.php | 2 - 29 files changed, 31 insertions(+), 80 deletions(-) diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index 743c585a6..64197f11b 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -3,6 +3,7 @@ namespace MongoDB\CodeGenerator\Command; +use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\CodeGenerator\Definition\ExpressionDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\ExpressionClassGenerator; @@ -33,20 +34,14 @@ public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Generating code for mongodb/mongodb library'); - $this->generateExpressionClasses($output); - - $config = require $this->configDir . '/operators.php'; - assert(is_array($config)); - - foreach ($config as $key => $def) { - assert(is_array($def)); - $this->generate($def, $output); - } + $expressions = $this->generateExpressionClasses($output); + $this->generateOperatorClasses($expressions, $output); return Command::SUCCESS; } - private function generateExpressionClasses(OutputInterface $output): void + /** @return array, ExpressionDefinition> */ + private function generateExpressionClasses(OutputInterface $output): array { $output->writeln('Generating expression classes'); @@ -63,23 +58,32 @@ private function generateExpressionClasses(OutputInterface $output): void $generator = new ExpressionFactoryGenerator($this->rootDir); $generator->generate($definitions); + + return $definitions; } - private function generate(array $def, OutputInterface $output): void + /** @param array, ExpressionDefinition> $expressions */ + private function generateOperatorClasses(array $expressions, OutputInterface $output): void { - $definition = new GeneratorDefinition(...$def); + $config = require $this->configDir . '/operators.php'; + assert(is_array($config)); - $output->writeln(sprintf('Generating classes for %s with %s', basename($definition->configFile), $definition->generatorClass)); + foreach ($config as $key => $def) { + assert(is_array($def)); + $definition = new GeneratorDefinition(...$def); - if (! class_exists($definition->generatorClass)) { - $output->writeln(sprintf('Generator class %s does not exist', $definition->generatorClass)); + $output->writeln(sprintf('Generating classes for %s with %s', basename($definition->configFile), $definition->generatorClass)); - return; - } + if (! class_exists($definition->generatorClass)) { + $output->writeln(sprintf('Generator class %s does not exist', $definition->generatorClass)); + + return; + } - $generatorClass = $definition->generatorClass; - $generator = new $generatorClass($this->rootDir); - assert($generator instanceof OperatorGenerator); - $generator->generate($definition); + $generatorClass = $definition->generatorClass; + $generator = new $generatorClass($this->rootDir, $expressions); + assert($generator instanceof OperatorGenerator); + $generator->generate($definition); + } } } diff --git a/generator/src/ExpressionClassGenerator.php b/generator/src/ExpressionClassGenerator.php index a045909ab..9a9dd75d9 100644 --- a/generator/src/ExpressionClassGenerator.php +++ b/generator/src/ExpressionClassGenerator.php @@ -4,13 +4,10 @@ namespace MongoDB\CodeGenerator; use MongoDB\CodeGenerator\Definition\ExpressionDefinition; -use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PhpNamespace; use Nette\PhpGenerator\Type; use function array_map; -use function sprintf; -use function str_contains; /** * Generates a value object class for expressions @@ -59,20 +56,6 @@ public function createClassOrInterface(ExpressionDefinition $definition): PhpNam $class->setExtends($definition->implements); } - // @todo add namespace use for types classes & interfaces - $types = array_map( - function (string $type): string|Literal { - if (str_contains($type, '\\')) { - return new Literal(sprintf('\\%s::class', $type)); - } - - return $type; - }, - $definition->types, - ); - - $class->addConstant('ACCEPTED_TYPES', $types); - return $namespace; } } diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index b8577feaa..8d7540588 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -6,17 +6,18 @@ use InvalidArgumentException; use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\CodeGenerator\Definition\ArgumentDefinition; +use MongoDB\CodeGenerator\Definition\ExpressionDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; use MongoDB\CodeGenerator\Definition\YamlReader; use Nette\PhpGenerator\Type; +use function array_key_exists; use function array_merge; use function array_unique; use function class_exists; use function in_array; use function interface_exists; -use function is_subclass_of; use function sort; use function sprintf; use function ucfirst; @@ -27,6 +28,8 @@ abstract class OperatorGenerator extends AbstractGenerator final public function __construct( string $rootDir, + /** @var array, ExpressionDefinition> */ + private array $expressions ) { parent::__construct($rootDir); @@ -55,7 +58,7 @@ final protected function getExpressionTypeInterface(string $type): string $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($type); - if (! is_subclass_of($interface, ExpressionInterface::class)) { + if (! array_key_exists($interface, $this->expressions)) { throw new InvalidArgumentException(sprintf('Invalid expression type "%s".', $type)); } @@ -72,7 +75,7 @@ final protected function generateExpressionTypes(ArgumentDefinition $arg): objec $nativeTypes = []; foreach ((array) $arg->type as $type) { $interface = $this->getExpressionTypeInterface($type); - $nativeTypes = array_merge($nativeTypes, [$interface], $interface::ACCEPTED_TYPES); + $nativeTypes = array_merge($nativeTypes, [$interface], $this->expressions[$interface]->types); } $docTypes = $nativeTypes = array_unique($nativeTypes); diff --git a/src/Builder/Expression/ArrayFieldPath.php b/src/Builder/Expression/ArrayFieldPath.php index ac01c909d..46d1fc380 100644 --- a/src/Builder/Expression/ArrayFieldPath.php +++ b/src/Builder/Expression/ArrayFieldPath.php @@ -8,8 +8,6 @@ class ArrayFieldPath extends FieldPath implements ResolvesToArray { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/BoolFieldPath.php b/src/Builder/Expression/BoolFieldPath.php index a6a320ad0..38c055af1 100644 --- a/src/Builder/Expression/BoolFieldPath.php +++ b/src/Builder/Expression/BoolFieldPath.php @@ -8,8 +8,6 @@ class BoolFieldPath extends FieldPath implements ResolvesToBool { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/DateFieldPath.php b/src/Builder/Expression/DateFieldPath.php index dbc7d8c42..a00664a2b 100644 --- a/src/Builder/Expression/DateFieldPath.php +++ b/src/Builder/Expression/DateFieldPath.php @@ -8,8 +8,6 @@ class DateFieldPath extends FieldPath implements ResolvesToDate { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/DecimalFieldPath.php b/src/Builder/Expression/DecimalFieldPath.php index 75a22506e..c8327df61 100644 --- a/src/Builder/Expression/DecimalFieldPath.php +++ b/src/Builder/Expression/DecimalFieldPath.php @@ -8,8 +8,6 @@ class DecimalFieldPath extends FieldPath implements ResolvesToDecimal { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/ExpressionInterface.php b/src/Builder/Expression/ExpressionInterface.php index 28a6a1c1a..bc1fa5551 100644 --- a/src/Builder/Expression/ExpressionInterface.php +++ b/src/Builder/Expression/ExpressionInterface.php @@ -8,5 +8,4 @@ interface ExpressionInterface { - public const ACCEPTED_TYPES = ['mixed']; } diff --git a/src/Builder/Expression/ExpressionObject.php b/src/Builder/Expression/ExpressionObject.php index 8573ccdf2..209acfbe0 100644 --- a/src/Builder/Expression/ExpressionObject.php +++ b/src/Builder/Expression/ExpressionObject.php @@ -8,5 +8,4 @@ interface ExpressionObject extends ExpressionInterface { - public const ACCEPTED_TYPES = ['array', 'stdClass', \MongoDB\BSON\Document::class, \MongoDB\BSON\Serializable::class]; } diff --git a/src/Builder/Expression/FieldPath.php b/src/Builder/Expression/FieldPath.php index 5285f2e19..15f6192d2 100644 --- a/src/Builder/Expression/FieldPath.php +++ b/src/Builder/Expression/FieldPath.php @@ -8,8 +8,6 @@ class FieldPath implements ExpressionInterface { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/FloatFieldPath.php b/src/Builder/Expression/FloatFieldPath.php index bb933f37d..d58f5ce06 100644 --- a/src/Builder/Expression/FloatFieldPath.php +++ b/src/Builder/Expression/FloatFieldPath.php @@ -8,8 +8,6 @@ class FloatFieldPath extends FieldPath implements ResolvesToFloat { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/IntFieldPath.php b/src/Builder/Expression/IntFieldPath.php index c77f51e05..f2242ffb2 100644 --- a/src/Builder/Expression/IntFieldPath.php +++ b/src/Builder/Expression/IntFieldPath.php @@ -8,8 +8,6 @@ class IntFieldPath extends FieldPath implements ResolvesToInt { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/Literal.php b/src/Builder/Expression/Literal.php index 1077c333a..0222123ba 100644 --- a/src/Builder/Expression/Literal.php +++ b/src/Builder/Expression/Literal.php @@ -8,8 +8,6 @@ class Literal implements ExpressionInterface { - public const ACCEPTED_TYPES = ['mixed']; - public mixed $expression; public function __construct(mixed $expression) diff --git a/src/Builder/Expression/NullFieldPath.php b/src/Builder/Expression/NullFieldPath.php index 69554ffac..05dc835c9 100644 --- a/src/Builder/Expression/NullFieldPath.php +++ b/src/Builder/Expression/NullFieldPath.php @@ -8,8 +8,6 @@ class NullFieldPath extends FieldPath implements ResolvesToNull { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/NumberFieldPath.php b/src/Builder/Expression/NumberFieldPath.php index 2efdb35f4..98177457c 100644 --- a/src/Builder/Expression/NumberFieldPath.php +++ b/src/Builder/Expression/NumberFieldPath.php @@ -8,8 +8,6 @@ class NumberFieldPath extends FieldPath implements ResolvesToNumber { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/ObjectFieldPath.php b/src/Builder/Expression/ObjectFieldPath.php index b08cfe1a2..70f69c134 100644 --- a/src/Builder/Expression/ObjectFieldPath.php +++ b/src/Builder/Expression/ObjectFieldPath.php @@ -8,8 +8,6 @@ class ObjectFieldPath extends FieldPath implements ResolvesToObject { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/Operator.php b/src/Builder/Expression/Operator.php index a82e783a7..dfeefd342 100644 --- a/src/Builder/Expression/Operator.php +++ b/src/Builder/Expression/Operator.php @@ -8,5 +8,4 @@ interface Operator extends ExpressionInterface { - public const ACCEPTED_TYPES = ['array', 'stdClass', \MongoDB\BSON\Document::class, \MongoDB\BSON\Serializable::class]; } diff --git a/src/Builder/Expression/ResolvesToArray.php b/src/Builder/Expression/ResolvesToArray.php index d23b48135..476fdf331 100644 --- a/src/Builder/Expression/ResolvesToArray.php +++ b/src/Builder/Expression/ResolvesToArray.php @@ -8,5 +8,4 @@ interface ResolvesToArray extends ExpressionInterface { - public const ACCEPTED_TYPES = ['list', \MongoDB\Model\BSONArray::class, \MongoDB\BSON\PackedArray::class]; } diff --git a/src/Builder/Expression/ResolvesToBool.php b/src/Builder/Expression/ResolvesToBool.php index 650db5ba8..6f81fb438 100644 --- a/src/Builder/Expression/ResolvesToBool.php +++ b/src/Builder/Expression/ResolvesToBool.php @@ -8,5 +8,4 @@ interface ResolvesToBool extends ExpressionInterface { - public const ACCEPTED_TYPES = ['bool']; } diff --git a/src/Builder/Expression/ResolvesToDate.php b/src/Builder/Expression/ResolvesToDate.php index 1b11cd1dc..87c276a5d 100644 --- a/src/Builder/Expression/ResolvesToDate.php +++ b/src/Builder/Expression/ResolvesToDate.php @@ -8,5 +8,4 @@ interface ResolvesToDate extends ExpressionInterface { - public const ACCEPTED_TYPES = ['DateTimeInterface', 'UTCDateTime']; } diff --git a/src/Builder/Expression/ResolvesToDecimal.php b/src/Builder/Expression/ResolvesToDecimal.php index 376594e1e..7ae2e5450 100644 --- a/src/Builder/Expression/ResolvesToDecimal.php +++ b/src/Builder/Expression/ResolvesToDecimal.php @@ -8,5 +8,4 @@ interface ResolvesToDecimal extends ResolvesToNumber { - public const ACCEPTED_TYPES = ['int', 'float', \MongoDB\BSON\Int64::class, \MongoDB\BSON\Decimal128::class]; } diff --git a/src/Builder/Expression/ResolvesToFloat.php b/src/Builder/Expression/ResolvesToFloat.php index 81b0a3e98..dc6b88297 100644 --- a/src/Builder/Expression/ResolvesToFloat.php +++ b/src/Builder/Expression/ResolvesToFloat.php @@ -8,5 +8,4 @@ interface ResolvesToFloat extends ResolvesToNumber { - public const ACCEPTED_TYPES = ['int', 'float', \MongoDB\BSON\Int64::class]; } diff --git a/src/Builder/Expression/ResolvesToInt.php b/src/Builder/Expression/ResolvesToInt.php index ce36a61bb..6d1631e33 100644 --- a/src/Builder/Expression/ResolvesToInt.php +++ b/src/Builder/Expression/ResolvesToInt.php @@ -8,5 +8,4 @@ interface ResolvesToInt extends ResolvesToNumber { - public const ACCEPTED_TYPES = ['int', \MongoDB\BSON\Int64::class]; } diff --git a/src/Builder/Expression/ResolvesToNull.php b/src/Builder/Expression/ResolvesToNull.php index ff8cd9061..0a984f437 100644 --- a/src/Builder/Expression/ResolvesToNull.php +++ b/src/Builder/Expression/ResolvesToNull.php @@ -8,5 +8,4 @@ interface ResolvesToNull extends ExpressionInterface { - public const ACCEPTED_TYPES = ['null']; } diff --git a/src/Builder/Expression/ResolvesToNumber.php b/src/Builder/Expression/ResolvesToNumber.php index 71e2ba187..132aa8d58 100644 --- a/src/Builder/Expression/ResolvesToNumber.php +++ b/src/Builder/Expression/ResolvesToNumber.php @@ -8,5 +8,4 @@ interface ResolvesToNumber extends ExpressionInterface { - public const ACCEPTED_TYPES = ['int', 'float', \MongoDB\BSON\Int64::class, \MongoDB\BSON\Decimal128::class]; } diff --git a/src/Builder/Expression/ResolvesToObject.php b/src/Builder/Expression/ResolvesToObject.php index 67a19fd1e..a59ad5797 100644 --- a/src/Builder/Expression/ResolvesToObject.php +++ b/src/Builder/Expression/ResolvesToObject.php @@ -8,5 +8,4 @@ interface ResolvesToObject extends ExpressionInterface { - public const ACCEPTED_TYPES = ['array', 'object', \MongoDB\BSON\Document::class, \MongoDB\BSON\Serializable::class]; } diff --git a/src/Builder/Expression/ResolvesToString.php b/src/Builder/Expression/ResolvesToString.php index f37df9d1a..0272f7ad9 100644 --- a/src/Builder/Expression/ResolvesToString.php +++ b/src/Builder/Expression/ResolvesToString.php @@ -8,5 +8,4 @@ interface ResolvesToString extends ExpressionInterface { - public const ACCEPTED_TYPES = ['string']; } diff --git a/src/Builder/Expression/StringFieldPath.php b/src/Builder/Expression/StringFieldPath.php index ad4aeb80f..c667f3065 100644 --- a/src/Builder/Expression/StringFieldPath.php +++ b/src/Builder/Expression/StringFieldPath.php @@ -8,8 +8,6 @@ class StringFieldPath extends FieldPath implements ResolvesToString { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) diff --git a/src/Builder/Expression/Variable.php b/src/Builder/Expression/Variable.php index cb2207f08..f2919254d 100644 --- a/src/Builder/Expression/Variable.php +++ b/src/Builder/Expression/Variable.php @@ -8,8 +8,6 @@ class Variable implements ExpressionInterface { - public const ACCEPTED_TYPES = ['string']; - public string $expression; public function __construct(string $expression) From 1084b1ad1531a6bafda3e45d54cac940b8c886a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 28 Sep 2023 23:14:20 +0200 Subject: [PATCH 18/31] Fix psalm issues --- generator/src/OperatorClassGenerator.php | 2 +- generator/src/OperatorGenerator.php | 2 +- psalm-baseline.xml | 24 ++++++++++++ psalm.xml.dist | 1 + src/Builder/Aggregation.php | 2 +- src/Builder/Aggregation/FilterAggregation.php | 4 +- src/Builder/BuilderEncoder.php | 37 +++++++++++++------ 7 files changed, 56 insertions(+), 16 deletions(-) diff --git a/generator/src/OperatorClassGenerator.php b/generator/src/OperatorClassGenerator.php index 30d98685a..390d38875 100644 --- a/generator/src/OperatorClassGenerator.php +++ b/generator/src/OperatorClassGenerator.php @@ -84,7 +84,7 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition if ($type->list) { $constuctor->addBody(<<name}) && ! \array_is_list(\${$argument->name})) { - throw new \InvalidArgumentException(\sprintf('Expected \${$argument->name} argument to be a list, got an associative array.')); + throw new \InvalidArgumentException('Expected \${$argument->name} argument to be a list, got an associative array.'); } PHP); } diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index 8d7540588..9cd90b9d1 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -86,7 +86,7 @@ final protected function generateExpressionTypes(ArgumentDefinition $arg): objec if ($typeName === 'list') { $listCheck = true; $nativeTypes[$key] = 'array'; - $docTypes[$key] = 'list'; + $docTypes[$key] = 'list'; $use[] = '\\' . ExpressionInterface::class; continue; } diff --git a/psalm-baseline.xml b/psalm-baseline.xml index dc98f2176..2657e5c58 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -46,6 +46,30 @@ stdClass + + + EncodeIfSupported + + + $value + $value + $value + + + $encoded[] + $expr + $expression + $field + $key + $key + $query + $result + $result[] + $stage + $val + $val + + self::CURSOR_NOT_FOUND diff --git a/psalm.xml.dist b/psalm.xml.dist index 7c61054ef..380d4e853 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -1,5 +1,6 @@ $input + * @param BSONArray|PackedArray|ResolvesToArray|list $input * @param ResolvesToBool|bool $cond * @param ResolvesToString|null|string $as * @param Int64|ResolvesToInt|int|null $limit diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php index a14925768..09ec41e6b 100644 --- a/src/Builder/Aggregation/FilterAggregation.php +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -26,7 +26,7 @@ class FilterAggregation implements ResolvesToArray public Int64|ResolvesToInt|int|null $limit; /** - * @param BSONArray|PackedArray|ResolvesToArray|list $input + * @param BSONArray|PackedArray|ResolvesToArray|list $input * @param ResolvesToBool|bool $cond * @param ResolvesToString|null|string $as * @param Int64|ResolvesToInt|int|null $limit @@ -38,7 +38,7 @@ public function __construct( Int64|ResolvesToInt|int|null $limit = null, ) { if (\is_array($input) && ! \array_is_list($input)) { - throw new \InvalidArgumentException(\sprintf('Expected $input argument to be a list, got an associative array.')); + throw new \InvalidArgumentException('Expected $input argument to be a list, got an associative array.'); } $this->input = $input; $this->cond = $cond; diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index 5aac3577d..2f157d793 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -17,9 +17,11 @@ use function array_is_list; use function array_merge; +use function get_object_vars; use function is_array; use function sprintf; +/** @template-implements Encoder */ class BuilderEncoder implements Encoder { use EncodeIfSupported; @@ -44,7 +46,7 @@ public function canEncode($value): bool /** * {@inheritdoc} */ - public function encode($value): array|stdClass|string|int|float|bool|null + public function encode($value): stdClass|array|string { if (! $this->canEncode($value)) { throw UnsupportedValueException::invalidEncodableValue($value); @@ -73,11 +75,11 @@ public function encode($value): array|stdClass|string|int|float|bool|null $result = new stdClass(); $result->_id = $this->encodeIfSupported($value->_id); // Specific: fields are encoded as a map of properties to their values at the top level as _id - foreach ($value->fields as $key => $val) { + foreach ($value->fields ?? [] as $key => $val) { $result->{$key} = $this->encodeIfSupported($val); } - return (object) [$value::NAME => $result]; + return $this->wrap($value, $result); } if ($value instanceof ProjectStage) { @@ -87,7 +89,7 @@ public function encode($value): array|stdClass|string|int|float|bool|null $result->{$key} = $this->encodeIfSupported($val); } - return (object) [$value::NAME => $result]; + return $this->wrap($value, $result); } if ($value instanceof OrQuery) { @@ -112,7 +114,7 @@ public function encode($value): array|stdClass|string|int|float|bool|null $result[] = $encodedQuery; } - return (object) [$value::NAME => $result]; + return $this->wrap($value, $result); } // The generic but incomplete encoding code @@ -133,34 +135,47 @@ public function encode($value): array|stdClass|string|int|float|bool|null private function encodeAsArray(ExpressionInterface|StageInterface $value): stdClass { $result = []; - foreach ($value as $val) { + /** @var mixed $val */ + foreach (get_object_vars($value) as $val) { $result[] = $this->encodeIfSupported($val); } - return (object) [$value::NAME => $result]; + return $this->wrap($value, $result); } private function encodeAsObject(ExpressionInterface|StageInterface $value): stdClass { $result = new stdClass(); - foreach ($value as $key => $val) { + /** @var mixed $val */ + foreach (get_object_vars($value) as $key => $val) { + /** @var mixed $val */ $val = $this->encodeIfSupported($val); + // @todo check for undefined value vs null if ($val !== null) { $result->{$key} = $val; } } - return (object) [$value::NAME => $result]; + return $this->wrap($value, $result); } private function encodeAsSingle(ExpressionInterface|StageInterface $value): stdClass { $result = []; - foreach ($value as $val) { + /** @var mixed $val */ + foreach (get_object_vars($value) as $val) { $result = $this->encodeIfSupported($val); break; } - return (object) [$value::NAME => $result]; + return $this->wrap($value, $result); + } + + private function wrap(ExpressionInterface|StageInterface $value, mixed $result): stdClass + { + $object = new stdClass(); + $object->{$value::NAME} = $result; + + return $object; } } From a3eef39539be4b0ff0059c10cccbc84a59d90ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 29 Sep 2023 10:59:46 +0200 Subject: [PATCH 19/31] Static analysis --- psalm-baseline.xml | 6 +++++- src/Builder/BuilderEncoder.php | 2 +- src/Builder/Pipeline.php | 30 ++++++++++++++++++++++++------ tests/Builder/BuilderCodecTest.php | 23 ++++++++++++++--------- tests/PedantryTest.php | 11 +++++++++-- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 2657e5c58..a06fddc08 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -65,7 +65,6 @@ $query $result $result[] - $stage $val $val @@ -83,6 +82,11 @@ + + + ($value is NativeType ? BSONType : $value) + + $options diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index 2f157d793..dc57f7b61 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -55,7 +55,7 @@ public function encode($value): stdClass|array|string // A pipeline is encoded as a list of stages if ($value instanceof Pipeline) { $encoded = []; - foreach ($value->stages as $stage) { + foreach ($value->getIterator() as $stage) { $encoded[] = $this->encodeIfSupported($stage); } diff --git a/src/Builder/Pipeline.php b/src/Builder/Pipeline.php index 039ff5f8b..19f400eb0 100644 --- a/src/Builder/Pipeline.php +++ b/src/Builder/Pipeline.php @@ -2,22 +2,40 @@ namespace MongoDB\Builder; +use ArrayIterator; +use IteratorAggregate; use MongoDB\Builder\Stage\StageInterface; use MongoDB\Exception\InvalidArgumentException; +use Traversable; use function array_is_list; +use function array_merge; -class Pipeline +/** @template-implements IteratorAggregate */ +class Pipeline implements IteratorAggregate { - public array $stages; + /** @var StageInterface[] */ + private array $stages = []; - public function __construct( - StageInterface ...$stages - ) { + public function __construct(StageInterface ...$stages) + { + $this->add(...$stages); + } + + /** @return $this */ + public function add(StageInterface ...$stages): static + { if (! array_is_list($stages)) { throw new InvalidArgumentException('Expected $stages argument to be a list, got an associative array.'); } - $this->stages = $stages; + $this->stages = array_merge($this->stages, $stages); + + return $this; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->stages); } } diff --git a/tests/Builder/BuilderCodecTest.php b/tests/Builder/BuilderCodecTest.php index d129d5495..dbf6b48e6 100644 --- a/tests/Builder/BuilderCodecTest.php +++ b/tests/Builder/BuilderCodecTest.php @@ -16,19 +16,23 @@ use function array_walk; use function is_array; +/** + * @todo This annotation is not enough as this PHP file needs to use named arguments, that can't compile on PHP 7.4 + * @requires PHP 8.0 + */ class BuilderCodecTest extends TestCase { + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#equality-match */ public function testPipeline(): void { $pipeline = new Pipeline( - Stage::match(Aggregation::eq('$foo', 1)), - Stage::match(Aggregation::ne('$foo', 2)), + // @todo array is accepted by the stage class, but we expect an object. The driver accepts both. + Stage::match((object) ['author' => 'dave']), Stage::limit(1), ); $expected = [ - ['$match' => ['$eq' => ['$foo', 1]]], - ['$match' => ['$ne' => ['$foo', 2]]], + ['$match' => ['author' => 'dave']], ['$limit' => 1], ]; @@ -70,15 +74,16 @@ public function testPerformCount(): void * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/#examples * @dataProvider provideAggregationFilterLimit */ - public function testAggregationFilter($limit, $expectedLimit): void + public function testAggregationFilter(?int $limit, array $expectedLimit): void { $pipeline = new Pipeline( Stage::project([ 'items' => Aggregation::filter( - input: Expression::arrayFieldPath('items'), - cond: Aggregation::gte(Expression::variable('item.price'), 100), - as: 'item', - limit: $limit, + // @todo use named argument once we can require PHP 8 + Expression::arrayFieldPath('items'), + Aggregation::gte(Expression::variable('item.price'), 100), + 'item', + $limit, ), ]), ); diff --git a/tests/PedantryTest.php b/tests/PedantryTest.php index 29df17a19..9f8845f06 100644 --- a/tests/PedantryTest.php +++ b/tests/PedantryTest.php @@ -13,12 +13,14 @@ use function realpath; use function str_contains; use function str_replace; +use function str_starts_with; use function strcasecmp; use function strlen; use function substr; use function usort; use const DIRECTORY_SEPARATOR; +use const PHP_VERSION_ID; /** * Pedantic tests that have nothing to do with functional correctness. @@ -26,7 +28,7 @@ class PedantryTest extends TestCase { /** @dataProvider provideProjectClassNames */ - public function testMethodsAreOrderedAlphabeticallyByVisibility($className): void + public function testMethodsAreOrderedAlphabeticallyByVisibility(string $className): void { $class = new ReflectionClass($className); $methods = $class->getMethods(); @@ -56,7 +58,7 @@ public function testMethodsAreOrderedAlphabeticallyByVisibility($className): voi $this->assertEquals($sortedMethods, $methods); } - public function provideProjectClassNames() + public static function provideProjectClassNames(): array { $classNames = []; $srcDir = realpath(__DIR__ . '/../src/'); @@ -74,6 +76,11 @@ public function provideProjectClassNames() } $className = 'MongoDB\\' . str_replace(DIRECTORY_SEPARATOR, '\\', substr($file->getRealPath(), strlen($srcDir) + 1, -4)); + + if (PHP_VERSION_ID < 80000 && str_starts_with($className, 'MongoDB\\Builder\\')) { + continue; + } + $classNames[$className][] = $className; } From 63aa36885f3a1d7a9b0b7f75d0f5bda58763a0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 12:59:15 +0200 Subject: [PATCH 20/31] Add FieldName type --- generator/config/expressions.php | 6 ++++++ src/Builder/Expression.php | 6 ++++++ src/Builder/Expression/FieldName.php | 17 +++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 src/Builder/Expression/FieldName.php diff --git a/generator/config/expressions.php b/generator/config/expressions.php index 0b448fec9..d11e30532 100644 --- a/generator/config/expressions.php +++ b/generator/config/expressions.php @@ -24,6 +24,12 @@ function typeFieldPath(string $resolvesTo): array ExpressionInterface::class => [ 'types' => ['mixed'], ], + // @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, diff --git a/src/Builder/Expression.php b/src/Builder/Expression.php index 267b1d234..c1d20dd83 100644 --- a/src/Builder/Expression.php +++ b/src/Builder/Expression.php @@ -10,6 +10,7 @@ use MongoDB\Builder\Expression\BoolFieldPath; use MongoDB\Builder\Expression\DateFieldPath; use MongoDB\Builder\Expression\DecimalFieldPath; +use MongoDB\Builder\Expression\FieldName; use MongoDB\Builder\Expression\FieldPath; use MongoDB\Builder\Expression\FloatFieldPath; use MongoDB\Builder\Expression\IntFieldPath; @@ -42,6 +43,11 @@ public static function decimalFieldPath(string $expression): DecimalFieldPath return new DecimalFieldPath($expression); } + public static function fieldName(string $expression): FieldName + { + return new FieldName($expression); + } + public static function fieldPath(string $expression): FieldPath { return new FieldPath($expression); diff --git a/src/Builder/Expression/FieldName.php b/src/Builder/Expression/FieldName.php new file mode 100644 index 000000000..d76dd6aa0 --- /dev/null +++ b/src/Builder/Expression/FieldName.php @@ -0,0 +1,17 @@ +expression = $expression; + } +} From 078d679e0977338105dd1256e69efec7889ca8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 12:59:55 +0200 Subject: [PATCH 21/31] Reractor operators config to use avoid redondancy --- generator/config/operators.php | 33 +++++++------------ generator/src/Command/GenerateCommand.php | 18 ++++------ .../src/Definition/GeneratorDefinition.php | 12 ++++--- generator/src/ExpressionClassGenerator.php | 6 ++-- 4 files changed, 27 insertions(+), 42 deletions(-) diff --git a/generator/config/operators.php b/generator/config/operators.php index 994919b4e..e9d1583e2 100644 --- a/generator/config/operators.php +++ b/generator/config/operators.php @@ -9,42 +9,33 @@ // Aggregation Pipeline Stages [ 'configFile' => __DIR__ . '/stages.yaml', - 'generatorClass' => OperatorClassGenerator::class, - 'namespace' => 'MongoDB\\Builder\\Stage', - 'classNameSuffix' => 'Stage', - ], - [ - 'configFile' => __DIR__ . '/stages.yaml', - 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Stage', 'classNameSuffix' => 'Stage', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + ], ], // Aggregation Pipeline Operators [ 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => OperatorClassGenerator::class, - 'namespace' => 'MongoDB\\Builder\\Aggregation', - 'classNameSuffix' => 'Aggregation', - ], - [ - 'configFile' => __DIR__ . '/pipeline-operators.yaml', - 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Aggregation', 'classNameSuffix' => 'Aggregation', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + ], ], // Query Operators [ 'configFile' => __DIR__ . '/query-operators.yaml', - 'generatorClass' => OperatorClassGenerator::class, - 'namespace' => 'MongoDB\\Builder\\Query', - 'classNameSuffix' => 'Query', - ], - [ - 'configFile' => __DIR__ . '/query-operators.yaml', - 'generatorClass' => OperatorFactoryGenerator::class, 'namespace' => 'MongoDB\\Builder\\Query', 'classNameSuffix' => 'Query', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + ], ], ]; diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index 64197f11b..67b7e8bed 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -68,22 +68,16 @@ private function generateOperatorClasses(array $expressions, OutputInterface $ou $config = require $this->configDir . '/operators.php'; assert(is_array($config)); - foreach ($config as $key => $def) { + foreach ($config as $def) { assert(is_array($def)); $definition = new GeneratorDefinition(...$def); - $output->writeln(sprintf('Generating classes for %s with %s', basename($definition->configFile), $definition->generatorClass)); - - if (! class_exists($definition->generatorClass)) { - $output->writeln(sprintf('Generator class %s does not exist', $definition->generatorClass)); - - return; + foreach ($definition->generators as $generatorClass) { + $output->writeln(sprintf('Generating classes for %s with %s', basename($definition->configFile), $generatorClass)); + $generator = new $generatorClass($this->rootDir, $expressions); + assert($generator instanceof OperatorGenerator); + $generator->generate($definition); } - - $generatorClass = $definition->generatorClass; - $generator = new $generatorClass($this->rootDir, $expressions); - assert($generator instanceof OperatorGenerator); - $generator->generate($definition); } } } diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index ad1f40f97..fcc125cb1 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -16,10 +16,10 @@ public function __construct( public readonly string $configFile, /** - * @var class-string - * @psalm-assert class-string + * @var list> + * @psalm-assert list> */ - public readonly string $generatorClass, + public readonly array $generators, public readonly string $namespace, public readonly string $classNameSuffix = '', public readonly array $interfaces = [], @@ -33,8 +33,10 @@ public function __construct( throw new InvalidArgumentException(sprintf('Namespace must not end with "\\". Got "%s".', $this->namespace)); } - if (! is_subclass_of($this->generatorClass, OperatorGenerator::class)) { - throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $this->generatorClass, OperatorGenerator::class)); + foreach ($this->generators as $class) { + if (! is_subclass_of($class, OperatorGenerator::class)) { + throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $class, OperatorGenerator::class)); + } } } } diff --git a/generator/src/ExpressionClassGenerator.php b/generator/src/ExpressionClassGenerator.php index 9a9dd75d9..ca6016697 100644 --- a/generator/src/ExpressionClassGenerator.php +++ b/generator/src/ExpressionClassGenerator.php @@ -38,11 +38,9 @@ public function createClassOrInterface(ExpressionDefinition $definition): PhpNam if ($definition->class) { $class = $namespace->addClass($className); $class->setImplements($definition->implements); - if ($definition->extends) { - $class->setExtends($definition->extends); - } + $class->setExtends($definition->extends); - // Replace with promoted property in PHP 8 + // Replace with promoted property in PHP 8.1 $propertyType = Type::union(...$types); $class->addProperty('expression') ->setType($propertyType) From 97e0329d4fc66756b9d1ae3d5fed02161559aa9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 14:18:30 +0200 Subject: [PATCH 22/31] Use assert instead of if...throw to validate config --- .gitattributes | 1 + generator/bin/console | 2 +- generator/composer.json | 2 +- generator/src/AbstractGenerator.php | 7 ++-- generator/src/Command/GenerateCommand.php | 6 ++-- .../src/Definition/ArgumentDefinition.php | 9 ++--- .../src/Definition/ExpressionDefinition.php | 6 ++-- .../src/Definition/GeneratorDefinition.php | 34 +++++++------------ .../src/Definition/OperatorDefinition.php | 12 ++----- generator/src/OperatorClassGenerator.php | 6 ++-- generator/src/OperatorGenerator.php | 12 +++---- src/Builder/BuilderEncoder.php | 6 ++-- 12 files changed, 41 insertions(+), 62 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0159e364a..64c458128 100644 --- a/.gitattributes +++ b/.gitattributes @@ -20,6 +20,7 @@ psalm-baseline.xml export-ignore # 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 diff --git a/generator/bin/console b/generator/bin/console index 0288aa279..eb0323cc6 100755 --- a/generator/bin/console +++ b/generator/bin/console @@ -12,5 +12,5 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')) { require __DIR__ . '/../vendor/autoload.php'; $application = new Application(); -$application->add(new GenerateCommand(__DIR__ . '/../../',__DIR__ . '/../config')); +$application->add(new GenerateCommand(__DIR__ . '/../../', __DIR__ . '/../config')); $application->run(); diff --git a/generator/composer.json b/generator/composer.json index faa365acf..7974a887e 100644 --- a/generator/composer.json +++ b/generator/composer.json @@ -14,7 +14,7 @@ }, "require": { "php": ">=8.2", - "ext-mongodb": "*", + "ext-mongodb": "^1.16.0", "mongodb/mongodb": "@dev", "nette/php-generator": "^4", "symfony/console": "^6.3", diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php index 02fb6f0b9..406184e47 100644 --- a/generator/src/AbstractGenerator.php +++ b/generator/src/AbstractGenerator.php @@ -10,6 +10,7 @@ use Nette\PhpGenerator\PsrPrinter; use function array_pop; +use function assert; use function count; use function current; use function dirname; @@ -48,15 +49,13 @@ final protected function splitNamespaceAndClassName(string $fqcn): array final protected function writeFile(PhpNamespace $namespace): void { $classes = $namespace->getClasses(); - if (count($classes) !== 1) { - throw new InvalidArgumentException(sprintf('Expected exactly one class in namespace "%s", got %d.', $namespace->getName(), count($classes))); - } + assert(count($classes) === 1, sprintf('Expected exactly one class in namespace "%s", got %d.', $namespace->getName(), count($classes))); $filename = $this->rootDir . '/' . $this->getFileName($namespace->getName(), current($classes)->getName()); $dirname = dirname($filename); if (! is_dir($dirname)) { - mkdir($dirname, 0775, true); + mkdir($dirname, 0755, true); } $file = new PhpFile(); diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index 67b7e8bed..fd1122994 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -14,9 +14,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function array_key_exists; use function assert; use function basename; -use function class_exists; +use function is_a; use function is_array; use function sprintf; @@ -52,6 +53,7 @@ private function generateExpressionClasses(OutputInterface $output): array $generator = new ExpressionClassGenerator($this->rootDir); foreach ($config as $name => $def) { assert(is_array($def)); + assert(! array_key_exists($name, $definitions), sprintf('Duplicate expression name "%s".', $name)); $definitions[$name] = $def = new ExpressionDefinition($name, ...$def); $generator->generate($def); } @@ -74,8 +76,8 @@ private function generateOperatorClasses(array $expressions, OutputInterface $ou foreach ($definition->generators as $generatorClass) { $output->writeln(sprintf('Generating classes for %s with %s', basename($definition->configFile), $generatorClass)); + assert(is_a($generatorClass, OperatorGenerator::class, true)); $generator = new $generatorClass($this->rootDir, $expressions); - assert($generator instanceof OperatorGenerator); $generator->generate($definition); } } diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index e004a7c1b..9390e9dd9 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -3,8 +3,7 @@ namespace MongoDB\CodeGenerator\Definition; -use InvalidArgumentException; - +use function assert; use function is_array; use function is_string; @@ -12,7 +11,7 @@ { public function __construct( public string $name, - /** @var string|list */ + /** @psalm-assert string|list $type */ public string|array $type, public bool $isOptional = false, public bool $isVariadic = false, @@ -20,9 +19,7 @@ public function __construct( ) { if (is_array($type)) { foreach ($type as $t) { - if (! is_string($t)) { - throw new InvalidArgumentException('Argument type must be a string or list of strings'); - } + assert(is_string($t)); } } } diff --git a/generator/src/Definition/ExpressionDefinition.php b/generator/src/Definition/ExpressionDefinition.php index 8dba36468..26c058f4b 100644 --- a/generator/src/Definition/ExpressionDefinition.php +++ b/generator/src/Definition/ExpressionDefinition.php @@ -3,7 +3,7 @@ namespace MongoDB\CodeGenerator\Definition; -use InvalidArgumentException; +use function assert; final readonly class ExpressionDefinition { @@ -16,8 +16,6 @@ public function __construct( /** @var list */ public array $implements = [], ) { - if ($extends && ! $class) { - throw new InvalidArgumentException('Cannot specify "extends" when "class" is not true'); - } + assert($class || ! $extends, 'Cannot specify "extends" when "class" is not true'); } } diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index fcc125cb1..c53a5c13b 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -3,9 +3,10 @@ namespace MongoDB\CodeGenerator\Definition; -use InvalidArgumentException; use MongoDB\CodeGenerator\OperatorGenerator; +use function assert; +use function is_string; use function is_subclass_of; use function sprintf; use function str_ends_with; @@ -14,29 +15,18 @@ final readonly class GeneratorDefinition { public function __construct( - public readonly string $configFile, - /** - * @var list> - * @psalm-assert list> - */ - public readonly array $generators, - public readonly string $namespace, - public readonly string $classNameSuffix = '', - public readonly array $interfaces = [], - public readonly ?string $parentClass = null, + public string $configFile, + /** @psalm-assert list> */ + public array $generators, + public string $namespace, + public string $classNameSuffix = '', + public array $interfaces = [], + public ?string $parentClass = null, ) { - if (! str_starts_with($this->namespace, 'MongoDB\\')) { - throw new InvalidArgumentException(sprintf('Namespace must start with "MongoDB\\". Got "%s".', $this->namespace)); - } - - if (str_ends_with($this->namespace, '\\')) { - throw new InvalidArgumentException(sprintf('Namespace must not end with "\\". Got "%s".', $this->namespace)); - } - + assert(str_starts_with($this->namespace, 'MongoDB\\'), sprintf('Namespace must start with "MongoDB\\". Got "%s"', $this->namespace)); + assert(! str_ends_with($this->namespace, '\\'), sprintf('Namespace must not end with "\\". Got "%s"', $this->namespace)); foreach ($this->generators as $class) { - if (! is_subclass_of($class, OperatorGenerator::class)) { - throw new InvalidArgumentException(sprintf('Generator class "%s" must extend "%s".', $class, OperatorGenerator::class)); - } + assert(is_string($class) && is_subclass_of($class, OperatorGenerator::class), sprintf('Generator class "%s" must extend "%s"', $class, OperatorGenerator::class)); } } } diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php index 76061f07b..3c8b5ae2c 100644 --- a/generator/src/Definition/OperatorDefinition.php +++ b/generator/src/Definition/OperatorDefinition.php @@ -3,9 +3,8 @@ namespace MongoDB\CodeGenerator\Definition; -use InvalidArgumentException; - use function array_map; +use function assert; use function count; use function in_array; use function sprintf; @@ -21,13 +20,8 @@ public function __construct( public ?string $type = null, array $args = [], ) { - if ($encode === null && count($args) !== 1) { - throw new InvalidArgumentException(sprintf('Operator "%s" have %s arguments, the "encode" parameter must be specified.', $this->name, count($args))); - } - - if (! in_array($this->encode, [null, 'array', 'object'], true)) { - throw new InvalidArgumentException(sprintf('Operator "%s" expect "encode" value to be "array" or "object". Got "%s".', $this->name, $this->encode)); - } + assert($encode || count($args) === 1, sprintf('Operator "%s" has %d arguments. The "encode" parameter must be specified.', $name, count($args))); + assert(in_array($encode, [null, 'array', 'object'], true), sprintf('Operator "%s" expect "encode" value to be "array" or "object". Got "%s".', $name, $encode)); $this->arguments = array_map( fn ($arg): ArgumentDefinition => new ArgumentDefinition(...$arg), diff --git a/generator/src/OperatorClassGenerator.php b/generator/src/OperatorClassGenerator.php index 390d38875..e3b153135 100644 --- a/generator/src/OperatorClassGenerator.php +++ b/generator/src/OperatorClassGenerator.php @@ -7,8 +7,8 @@ use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; use Nette\PhpGenerator\PhpNamespace; -use RuntimeException; +use function assert; use function interface_exists; use function sprintf; @@ -110,9 +110,7 @@ private function getInterfaces(OperatorDefinition $definition): array } $interface = $this->getExpressionTypeInterface($definition->type); - if (! interface_exists($interface)) { - throw new RuntimeException(sprintf('"%s" is not an interface.', $interface)); - } + assert(interface_exists($interface), sprintf('"%s" is not an interface.', $interface)); return [$interface]; } diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index 9cd90b9d1..2b899397d 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -3,7 +3,6 @@ namespace MongoDB\CodeGenerator; -use InvalidArgumentException; use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\CodeGenerator\Definition\ArgumentDefinition; use MongoDB\CodeGenerator\Definition\ExpressionDefinition; @@ -15,6 +14,7 @@ use function array_key_exists; use function array_merge; use function array_unique; +use function assert; use function class_exists; use function in_array; use function interface_exists; @@ -57,16 +57,14 @@ final protected function getExpressionTypeInterface(string $type): string } $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($type); - - if (! array_key_exists($interface, $this->expressions)) { - throw new InvalidArgumentException(sprintf('Invalid expression type "%s".', $type)); - } + assert(array_key_exists($interface, $this->expressions), sprintf('Invalid expression type "%s".', $type)); return $interface; } /** - * Expression types can contain class names, interface, native types or "list" + * Expression types can contain class names, interface, native types or "list". + * PHPDoc types are more precise than native types, so we use them systematically even if redundant. * * @return object{native:string,doc:string,use:list,list:bool} */ @@ -83,9 +81,11 @@ final protected function generateExpressionTypes(ArgumentDefinition $arg): objec $use = []; foreach ($nativeTypes as $key => $typeName) { + // "list" is a special type of array that needs to be checked in the code if ($typeName === 'list') { $listCheck = true; $nativeTypes[$key] = 'array'; + // @todo allow to specify the type of the elements in the list $docTypes[$key] = 'list'; $use[] = '\\' . ExpressionInterface::class; continue; diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index dc57f7b61..15f81a7fe 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -119,13 +119,13 @@ public function encode($value): stdClass|array|string // The generic but incomplete encoding code switch ($value::ENCODE) { - case self::ENCODE_AS_SINGLE: + case 'single': return $this->encodeAsSingle($value); - case self::ENCODE_AS_ARRAY: + case 'array': return $this->encodeAsArray($value); - case self::ENCODE_AS_OBJECT: + case 'object': return $this->encodeAsObject($value); } From e62d243f86d8e18033c33dd4507327b701c942f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 14:35:57 +0200 Subject: [PATCH 23/31] Add Aggregation Builder example --- examples/aggregation-builder.php | 85 +++++++++++++++++++ generator/config/pipeline-operators.yaml | 26 ++++++ src/Builder/Aggregation.php | 38 +++++++++ src/Builder/Aggregation/MaxAggregation.php | 25 ++++++ src/Builder/Aggregation/MinAggregation.php | 25 ++++++ src/Builder/Aggregation/ModAggregation.php | 28 ++++++ .../Aggregation/SubtractAggregation.php | 28 ++++++ src/Builder/BuilderEncoder.php | 9 -- tests/ExamplesTest.php | 9 ++ 9 files changed, 264 insertions(+), 9 deletions(-) create mode 100644 examples/aggregation-builder.php create mode 100644 src/Builder/Aggregation/MaxAggregation.php create mode 100644 src/Builder/Aggregation/MinAggregation.php create mode 100644 src/Builder/Aggregation/ModAggregation.php create mode 100644 src/Builder/Aggregation/SubtractAggregation.php diff --git a/examples/aggregation-builder.php b/examples/aggregation-builder.php new file mode 100644 index 000000000..ca3b35fc8 --- /dev/null +++ b/examples/aggregation-builder.php @@ -0,0 +1,85 @@ +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, + // @todo accept named arguments for $fields + fields: [ + '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); + +foreach ($cursor as $document) { + assert(is_object($document)); + printf("%s\n", toJSON($document)); +} diff --git a/generator/config/pipeline-operators.yaml b/generator/config/pipeline-operators.yaml index 86c9e7da9..9e2af1fc8 100644 --- a/generator/config/pipeline-operators.yaml +++ b/generator/config/pipeline-operators.yaml @@ -64,3 +64,29 @@ args: - name: expression type: expression +- name: min + type: expression + args: + - name: expression + type: expression +- name: max + type: expression + args: + - name: expression + type: expression +- name: subtract + type: expression + encode: array + args: + - name: expression1 + type: expression + - name: expression2 + type: expression +- name: mod + type: expression + encode: array + args: + - name: expression1 + type: expression + - name: expression2 + type: expression diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php index 596b159a6..4b2ac476a 100644 --- a/src/Builder/Aggregation.php +++ b/src/Builder/Aggregation.php @@ -14,7 +14,11 @@ use MongoDB\Builder\Aggregation\GtAggregation; use MongoDB\Builder\Aggregation\GteAggregation; use MongoDB\Builder\Aggregation\LtAggregation; +use MongoDB\Builder\Aggregation\MaxAggregation; +use MongoDB\Builder\Aggregation\MinAggregation; +use MongoDB\Builder\Aggregation\ModAggregation; use MongoDB\Builder\Aggregation\NeAggregation; +use MongoDB\Builder\Aggregation\SubtractAggregation; use MongoDB\Builder\Aggregation\SumAggregation; use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToArray; @@ -85,6 +89,31 @@ public static function lt(mixed $expression1, mixed $expression2): LtAggregation return new LtAggregation($expression1, $expression2); } + /** + * @param ExpressionInterface|mixed $expression + */ + public static function max(mixed $expression): MaxAggregation + { + return new MaxAggregation($expression); + } + + /** + * @param ExpressionInterface|mixed $expression + */ + public static function min(mixed $expression): MinAggregation + { + return new MinAggregation($expression); + } + + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ + public static function mod(mixed $expression1, mixed $expression2): ModAggregation + { + return new ModAggregation($expression1, $expression2); + } + /** * @param ExpressionInterface|mixed $expression1 * @param ExpressionInterface|mixed $expression2 @@ -94,6 +123,15 @@ public static function ne(mixed $expression1, mixed $expression2): NeAggregation return new NeAggregation($expression1, $expression2); } + /** + * @param ExpressionInterface|mixed $expression1 + * @param ExpressionInterface|mixed $expression2 + */ + public static function subtract(mixed $expression1, mixed $expression2): SubtractAggregation + { + return new SubtractAggregation($expression1, $expression2); + } + /** * @param ExpressionInterface|mixed $expression */ diff --git a/src/Builder/Aggregation/MaxAggregation.php b/src/Builder/Aggregation/MaxAggregation.php new file mode 100644 index 000000000..97f0d66bd --- /dev/null +++ b/src/Builder/Aggregation/MaxAggregation.php @@ -0,0 +1,25 @@ +expression = $expression; + } +} diff --git a/src/Builder/Aggregation/MinAggregation.php b/src/Builder/Aggregation/MinAggregation.php new file mode 100644 index 000000000..43db134af --- /dev/null +++ b/src/Builder/Aggregation/MinAggregation.php @@ -0,0 +1,25 @@ +expression = $expression; + } +} diff --git a/src/Builder/Aggregation/ModAggregation.php b/src/Builder/Aggregation/ModAggregation.php new file mode 100644 index 000000000..082532895 --- /dev/null +++ b/src/Builder/Aggregation/ModAggregation.php @@ -0,0 +1,28 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } +} diff --git a/src/Builder/Aggregation/SubtractAggregation.php b/src/Builder/Aggregation/SubtractAggregation.php new file mode 100644 index 000000000..b3d2581ed --- /dev/null +++ b/src/Builder/Aggregation/SubtractAggregation.php @@ -0,0 +1,28 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } +} diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index 15f81a7fe..f58ea30d9 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -26,15 +26,6 @@ class BuilderEncoder implements Encoder { use EncodeIfSupported; - /** The first property is the operator value */ - public const ENCODE_AS_SINGLE = 'single'; - - /** Arguments as encoded as a map of properties to their values */ - public const ENCODE_AS_OBJECT = 'object'; - - /** Properties are encoded as a list of values, names are ignored */ - public const ENCODE_AS_ARRAY = 'array'; - /** * {@inheritdoc} */ diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index c00568f37..18b4428f4 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -10,6 +10,8 @@ use function random_bytes; use function sprintf; +use const PHP_VERSION_ID; + /** @runTestsInSeparateProcesses */ final class ExamplesTest extends FunctionalTestCase { @@ -43,6 +45,13 @@ public static function provideExamples(): Generator 'expectedOutput' => $expectedOutput, ]; + if (PHP_VERSION_ID >= 80000) { + yield 'aggregation-builder' => [ + 'file' => __DIR__ . '/../examples/aggregation-builder.php', + 'expectedOutput' => $expectedOutput, + ]; + } + $expectedOutput = <<<'OUTPUT' %s %s From fdf8f5a3af8ce601c80e101856ec133eaf853737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 15:15:41 +0200 Subject: [PATCH 24/31] Implement variadic options --- examples/aggregation-builder.php | 37 +++++++++---------- generator/config/expressions.php | 6 +++ generator/config/stages.yaml | 13 +++---- generator/src/Command/GenerateCommand.php | 2 +- .../src/Definition/ArgumentDefinition.php | 15 +++++++- .../src/Definition/ExpressionDefinition.php | 1 + generator/src/ExpressionClassGenerator.php | 4 ++ generator/src/OperatorGenerator.php | 13 ++++++- src/Builder/Aggregation.php | 2 +- src/Builder/Aggregation/FilterAggregation.php | 2 +- src/Builder/Stage.php | 12 +++--- src/Builder/Stage/GroupStage.php | 11 +++--- src/Builder/Stage/ProjectStage.php | 15 +++++--- tests/Builder/BuilderCodecTest.php | 12 +++--- tests/ExamplesTest.php | 2 +- 15 files changed, 89 insertions(+), 58 deletions(-) diff --git a/examples/aggregation-builder.php b/examples/aggregation-builder.php index ca3b35fc8..3947a2d2b 100644 --- a/examples/aggregation-builder.php +++ b/examples/aggregation-builder.php @@ -45,31 +45,28 @@ function toJSON(object $document): string $pipeline = new Pipeline( Stage::group( _id: null, - // @todo accept named arguments for $fields - fields: [ - 'totalCount' => Aggregation::sum(1), - 'evenCount' => Aggregation::sum( + 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, ), ), - 'oddCount' => Aggregation::sum( - Aggregation::subtract( - 1, - Aggregation::mod( - Expression::fieldPath('randomValue'), - 2, - ), - ), - ), - 'maxValue' => Aggregation::max( - Expression::fieldPath('randomValue'), - ), - 'minValue' => Aggregation::min( - Expression::fieldPath('randomValue'), - ), - ], + ), + maxValue: Aggregation::max( + Expression::fieldPath('randomValue'), + ), + minValue: Aggregation::min( + Expression::fieldPath('randomValue'), + ), ), ); diff --git a/generator/config/expressions.php b/generator/config/expressions.php index d11e30532..916fa9d5d 100644 --- a/generator/config/expressions.php +++ b/generator/config/expressions.php @@ -20,6 +20,12 @@ function typeFieldPath(string $resolvesTo): array } 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'], diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index 26ca19944..7ed4d7919 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -17,16 +17,15 @@ type: stage encode: object args: - # @todo _id is not optional but only nullable - name: _id - type: expression - isOptional: true - # @todo fields are optional and encoded at the same level as _id + type: [expression, "null"] - name: fields - type: resolvesToObject - isOptional: true + type: expression + isVariadic: true + variadicMin: 0 - name: project type: stage args: - name: specifications - type: resolvesToObject + type: expression + isVariadic: true diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index fd1122994..dbd15f8c6 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -41,7 +41,7 @@ public function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } - /** @return array, ExpressionDefinition> */ + /** @return array */ private function generateExpressionClasses(OutputInterface $output): array { $output->writeln('Generating expression classes'); diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index 9390e9dd9..ae82dfb81 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -9,18 +9,29 @@ final readonly class ArgumentDefinition { + public ?int $variadicMin; + public function __construct( public string $name, /** @psalm-assert string|list $type */ public string|array $type, public bool $isOptional = false, public bool $isVariadic = false, - public int $variadicMin = 1, + ?int $variadicMin = null, ) { if (is_array($type)) { foreach ($type as $t) { - assert(is_string($t)); + assert(is_string($t), json_encode($type)); } } + + if (! $isVariadic) { + assert($variadicMin === null); + $this->variadicMin = null; + } elseif ($variadicMin === null) { + $this->variadicMin = $isOptional ? 0 : 1; + } else { + $this->variadicMin = $variadicMin; + } } } diff --git a/generator/src/Definition/ExpressionDefinition.php b/generator/src/Definition/ExpressionDefinition.php index 26c058f4b..f5be42116 100644 --- a/generator/src/Definition/ExpressionDefinition.php +++ b/generator/src/Definition/ExpressionDefinition.php @@ -11,6 +11,7 @@ public function __construct( public string $name, /** @var list */ public array $types, + public bool $scalar = false, public bool $class = false, public ?string $extends = null, /** @var list */ diff --git a/generator/src/ExpressionClassGenerator.php b/generator/src/ExpressionClassGenerator.php index ca6016697..cd988bc3f 100644 --- a/generator/src/ExpressionClassGenerator.php +++ b/generator/src/ExpressionClassGenerator.php @@ -16,6 +16,10 @@ class ExpressionClassGenerator extends AbstractGenerator { public function generate(ExpressionDefinition $definition): void { + if ($definition->scalar) { + return; + } + $this->writeFile($this->createClassOrInterface($definition)); } diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index 2b899397d..1b5b65dbb 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -49,13 +49,18 @@ final protected function getOperatorClassName(GeneratorDefinition $definition, O return ucfirst($operator->name) . $definition->classNameSuffix; } - /** @return class-string */ + /** @return class-string|string */ final protected function getExpressionTypeInterface(string $type): string { if ('expression' === $type) { return ExpressionInterface::class; } + // Scalar types + if (array_key_exists($type, $this->expressions)) { + return $type; + } + $interface = 'MongoDB\\Builder\\Expression\\' . ucfirst($type); assert(array_key_exists($interface, $this->expressions), sprintf('Invalid expression type "%s".', $type)); @@ -91,9 +96,13 @@ final protected function generateExpressionTypes(ArgumentDefinition $arg): objec continue; } + // strings cannot be empty + if ($typeName === 'string') { + $docTypes[$key] = 'non-empty-string'; + } + if (interface_exists($typeName) || class_exists($typeName)) { $use[] = $nativeTypes[$key] = '\\' . $typeName; - //$nativeTypes[$key] = $docTypes[$key] = '\\' . $typeName; $docTypes[$key] = $this->splitNamespaceAndClassName($typeName)[1]; // A union cannot contain both object and a class type if (in_array('object', $nativeTypes, true)) { diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php index 4b2ac476a..357e7c709 100644 --- a/src/Builder/Aggregation.php +++ b/src/Builder/Aggregation.php @@ -49,7 +49,7 @@ public static function eq(mixed $expression1, mixed $expression2): EqAggregation /** * @param BSONArray|PackedArray|ResolvesToArray|list $input * @param ResolvesToBool|bool $cond - * @param ResolvesToString|null|string $as + * @param ResolvesToString|non-empty-string|null $as * @param Int64|ResolvesToInt|int|null $limit */ public static function filter( diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php index 09ec41e6b..f48842d20 100644 --- a/src/Builder/Aggregation/FilterAggregation.php +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -28,7 +28,7 @@ class FilterAggregation implements ResolvesToArray /** * @param BSONArray|PackedArray|ResolvesToArray|list $input * @param ResolvesToBool|bool $cond - * @param ResolvesToString|null|string $as + * @param ResolvesToString|non-empty-string|null $as * @param Int64|ResolvesToInt|int|null $limit */ public function __construct( diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index 02d97fe02..493aa3f1c 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -22,11 +22,11 @@ final class Stage { /** * @param ExpressionInterface|mixed|null $_id - * @param Document|ResolvesToObject|Serializable|array|null|object $fields + * @param ExpressionInterface|mixed ...$fields */ - public static function group(mixed $_id = null, array|null|object $fields = null): GroupStage + public static function group(mixed $_id, mixed ...$fields): GroupStage { - return new GroupStage($_id, $fields); + return new GroupStage($_id, ...$fields); } /** @@ -46,11 +46,11 @@ public static function match(mixed $query): MatchStage } /** - * @param Document|ResolvesToObject|Serializable|array|object $specifications + * @param ExpressionInterface|mixed ...$specifications */ - public static function project(array|object $specifications): ProjectStage + public static function project(mixed ...$specifications): ProjectStage { - return new ProjectStage($specifications); + return new ProjectStage(...$specifications); } /** diff --git a/src/Builder/Stage/GroupStage.php b/src/Builder/Stage/GroupStage.php index 4bb9d32d1..395dee6ed 100644 --- a/src/Builder/Stage/GroupStage.php +++ b/src/Builder/Stage/GroupStage.php @@ -6,10 +6,7 @@ namespace MongoDB\Builder\Stage; -use MongoDB\BSON\Document; -use MongoDB\BSON\Serializable; use MongoDB\Builder\Expression\ExpressionInterface; -use MongoDB\Builder\Expression\ResolvesToObject; class GroupStage implements StageInterface { @@ -17,13 +14,15 @@ class GroupStage implements StageInterface public const ENCODE = 'object'; public mixed $_id; - public array|null|object $fields; + + /** @param list ...$fields */ + public array $fields; /** * @param ExpressionInterface|mixed|null $_id - * @param Document|ResolvesToObject|Serializable|array|null|object $fields + * @param ExpressionInterface|mixed $fields */ - public function __construct(mixed $_id = null, array|null|object $fields = null) + public function __construct(mixed $_id, mixed ...$fields) { $this->_id = $_id; $this->fields = $fields; diff --git a/src/Builder/Stage/ProjectStage.php b/src/Builder/Stage/ProjectStage.php index d51d8efe6..98a4feb69 100644 --- a/src/Builder/Stage/ProjectStage.php +++ b/src/Builder/Stage/ProjectStage.php @@ -6,22 +6,25 @@ namespace MongoDB\Builder\Stage; -use MongoDB\BSON\Document; -use MongoDB\BSON\Serializable; -use MongoDB\Builder\Expression\ResolvesToObject; +use MongoDB\Builder\Expression\ExpressionInterface; class ProjectStage implements StageInterface { public const NAME = '$project'; public const ENCODE = 'single'; - public array|object $specifications; + /** @param list ...$specifications */ + public array $specifications; /** - * @param Document|ResolvesToObject|Serializable|array|object $specifications + * @param ExpressionInterface|mixed $specifications */ - public function __construct(array|object $specifications) + public function __construct(mixed ...$specifications) { + if (\count($specifications) < 1) { + throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', 1, \count($specifications))); + } + $this->specifications = $specifications; } } diff --git a/tests/Builder/BuilderCodecTest.php b/tests/Builder/BuilderCodecTest.php index dbf6b48e6..e13471b99 100644 --- a/tests/Builder/BuilderCodecTest.php +++ b/tests/Builder/BuilderCodecTest.php @@ -47,7 +47,10 @@ public function testPerformCount(): void ['score' => [Query::gt(70), Query::lt(90)]], ['views' => Query::gte(1000)], )), - Stage::group(null, ['count' => Aggregation::sum(1)]), + Stage::group( + _id: null, + count: Aggregation::sum(1), + ), ); $expected = [ @@ -77,15 +80,14 @@ public function testPerformCount(): void public function testAggregationFilter(?int $limit, array $expectedLimit): void { $pipeline = new Pipeline( - Stage::project([ - 'items' => Aggregation::filter( - // @todo use named argument once we can require PHP 8 + Stage::project( + items: Aggregation::filter( Expression::arrayFieldPath('items'), Aggregation::gte(Expression::variable('item.price'), 100), 'item', $limit, ), - ]), + ), ); $expected = [ diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 18b4428f4..0fe2a2ec1 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -45,7 +45,7 @@ public static function provideExamples(): Generator 'expectedOutput' => $expectedOutput, ]; - if (PHP_VERSION_ID >= 80000) { + if (PHP_VERSION_ID >= 80100) { yield 'aggregation-builder' => [ 'file' => __DIR__ . '/../examples/aggregation-builder.php', 'expectedOutput' => $expectedOutput, From 55f8cce7e0a869cb5e81fa89cd36a13bd670338d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 15:16:47 +0200 Subject: [PATCH 25/31] Typo namespace --- examples/aggregation-builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/aggregation-builder.php b/examples/aggregation-builder.php index 3947a2d2b..5f7505db5 100644 --- a/examples/aggregation-builder.php +++ b/examples/aggregation-builder.php @@ -5,7 +5,7 @@ * This example demonstrates how you can use the builder provided by this library to build an aggregation pipeline. */ -namespace MongoDB\Examples\AggregatationBuilder; +namespace MongoDB\Examples\AggregationBuilder; use MongoDB\Builder\Aggregation; use MongoDB\Builder\BuilderEncoder; From ace7f603a6aa2cdda70622d4f6e29ed0f3e55893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 15:42:38 +0200 Subject: [PATCH 26/31] Assert types is a list of strings --- generator/src/Definition/ArgumentDefinition.php | 6 +++++- generator/src/Definition/GeneratorDefinition.php | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php index ae82dfb81..bfb5eeef4 100644 --- a/generator/src/Definition/ArgumentDefinition.php +++ b/generator/src/Definition/ArgumentDefinition.php @@ -3,9 +3,12 @@ namespace MongoDB\CodeGenerator\Definition; +use function array_is_list; use function assert; +use function get_debug_type; use function is_array; use function is_string; +use function sprintf; final readonly class ArgumentDefinition { @@ -20,8 +23,9 @@ public function __construct( ?int $variadicMin = null, ) { if (is_array($type)) { + assert(array_is_list($type), 'Type must be a list or a single string'); foreach ($type as $t) { - assert(is_string($t), json_encode($type)); + assert(is_string($t), sprintf('Type must be a list of strings. Got %s', get_debug_type($type))); } } diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php index c53a5c13b..936136f47 100644 --- a/generator/src/Definition/GeneratorDefinition.php +++ b/generator/src/Definition/GeneratorDefinition.php @@ -5,7 +5,9 @@ use MongoDB\CodeGenerator\OperatorGenerator; +use function array_is_list; use function assert; +use function class_exists; use function is_string; use function is_subclass_of; use function sprintf; @@ -23,9 +25,16 @@ public function __construct( public array $interfaces = [], public ?string $parentClass = null, ) { - assert(str_starts_with($this->namespace, 'MongoDB\\'), sprintf('Namespace must start with "MongoDB\\". Got "%s"', $this->namespace)); - assert(! str_ends_with($this->namespace, '\\'), sprintf('Namespace must not end with "\\". Got "%s"', $this->namespace)); - foreach ($this->generators as $class) { + assert(str_starts_with($namespace, 'MongoDB\\'), sprintf('Namespace must start with "MongoDB\\". Got "%s"', $namespace)); + assert(! str_ends_with($namespace, '\\'), sprintf('Namespace must not end with "\\". Got "%s"', $namespace)); + + assert(array_is_list($interfaces), 'Generators must be a list of class names'); + foreach ($interfaces as $interface) { + assert(is_string($interface) && class_exists($interface), sprintf('Interface "%s" does not exist', $interface)); + } + + assert(array_is_list($generators), 'Generators must be a list of class names'); + foreach ($generators as $class) { assert(is_string($class) && is_subclass_of($class, OperatorGenerator::class), sprintf('Generator class "%s" must extend "%s"', $class, OperatorGenerator::class)); } } From cae32c0219ba164395125bd9758c35379ff1c948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 15:53:44 +0200 Subject: [PATCH 27/31] Make generator a single-command app --- generator/README.md | 2 +- generator/{bin/console => generate} | 5 +++-- generator/src/Command/GenerateCommand.php | 9 +++++++-- phpcs.xml.dist | 1 - 4 files changed, 11 insertions(+), 6 deletions(-) rename generator/{bin/console => generate} (67%) diff --git a/generator/README.md b/generator/README.md index 2d8175598..8f2aab0fc 100644 --- a/generator/README.md +++ b/generator/README.md @@ -11,7 +11,7 @@ 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: `bin/console generate` +1. Run the generator: `./generate` ## Configuration diff --git a/generator/bin/console b/generator/generate similarity index 67% rename from generator/bin/console rename to generator/generate index eb0323cc6..017dbef6e 100755 --- a/generator/bin/console +++ b/generator/generate @@ -9,8 +9,9 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')) { exit(1); } -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/vendor/autoload.php'; $application = new Application(); -$application->add(new GenerateCommand(__DIR__ . '/../../', __DIR__ . '/../config')); +$application->add(new GenerateCommand(__DIR__ . '/../', __DIR__ . '/config')); +$application->setDefaultCommand('generate'); $application->run(); diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php index dbd15f8c6..faa4a13d7 100644 --- a/generator/src/Command/GenerateCommand.php +++ b/generator/src/Command/GenerateCommand.php @@ -9,7 +9,6 @@ use MongoDB\CodeGenerator\ExpressionClassGenerator; use MongoDB\CodeGenerator\ExpressionFactoryGenerator; use MongoDB\CodeGenerator\OperatorGenerator; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -21,7 +20,6 @@ use function is_array; use function sprintf; -#[AsCommand(name: 'generate', description: 'Generate code for mongodb/mongodb library')] final class GenerateCommand extends Command { public function __construct( @@ -31,6 +29,13 @@ public function __construct( parent::__construct(); } + public function configure(): void + { + $this->setName('generate'); + $this->setDescription('Generate code for mongodb/mongodb library'); + $this->setHelp('Generate code for mongodb/mongodb library'); + } + public function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Generating code for mongodb/mongodb library'); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index c852eef2d..949748155 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -15,7 +15,6 @@ examples generator/src generator/config - generator/bin tests tools rector.php From 0e80bb6c989ef537607352524c680916d476b153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 16:10:39 +0200 Subject: [PATCH 28/31] Add Optional::Undefined for optional values --- generator/src/OperatorClassGenerator.php | 3 ++- generator/src/OperatorFactoryGenerator.php | 3 ++- generator/src/OperatorGenerator.php | 6 ++++-- src/Builder/Aggregation.php | 8 ++++---- src/Builder/Aggregation/FilterAggregation.php | 13 +++++++------ src/Builder/BuilderEncoder.php | 3 +-- src/Builder/Optional.php | 15 +++++++++++++++ tests/Builder/BuilderCodecTest.php | 8 ++++---- 8 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 src/Builder/Optional.php diff --git a/generator/src/OperatorClassGenerator.php b/generator/src/OperatorClassGenerator.php index e3b153135..2c65d4075 100644 --- a/generator/src/OperatorClassGenerator.php +++ b/generator/src/OperatorClassGenerator.php @@ -6,6 +6,7 @@ use MongoDB\Builder\Stage\StageInterface; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; +use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PhpNamespace; use function assert; @@ -75,7 +76,7 @@ public function createClass(GeneratorDefinition $definition, OperatorDefinition PHP); } } elseif ($argument->isOptional) { - $constuctorParam->setDefaultValue(null); + $constuctorParam->setDefaultValue(new Literal('Optional::Undefined')); } $constuctor->addComment('@param ' . $type->doc . ' $' . $argument->name); diff --git a/generator/src/OperatorFactoryGenerator.php b/generator/src/OperatorFactoryGenerator.php index dc2d0bb39..b7e63594c 100644 --- a/generator/src/OperatorFactoryGenerator.php +++ b/generator/src/OperatorFactoryGenerator.php @@ -5,6 +5,7 @@ use MongoDB\CodeGenerator\Definition\GeneratorDefinition; use MongoDB\CodeGenerator\Definition\OperatorDefinition; +use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PhpNamespace; use function implode; @@ -52,7 +53,7 @@ private function createFactoryClass(GeneratorDefinition $definition): PhpNamespa $args[] = '...$' . $argument->name; } else { if ($argument->isOptional) { - $parameter->setDefaultValue(null); + $parameter->setDefaultValue(new Literal('Optional::Undefined')); } $method->addComment('@param ' . $type->doc . ' $' . $argument->name); diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index 1b5b65dbb..bcd0b26dc 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -4,6 +4,7 @@ namespace MongoDB\CodeGenerator; use MongoDB\Builder\Expression\ExpressionInterface; +use MongoDB\Builder\Optional; use MongoDB\CodeGenerator\Definition\ArgumentDefinition; use MongoDB\CodeGenerator\Definition\ExpressionDefinition; use MongoDB\CodeGenerator\Definition\GeneratorDefinition; @@ -112,8 +113,9 @@ final protected function generateExpressionTypes(ArgumentDefinition $arg): objec } if ($arg->isOptional) { - $nativeTypes[] = 'null'; - $docTypes[] = 'null'; + $use[] = '\\' . Optional::class; + $docTypes[] = 'Optional'; + $nativeTypes[] = Optional::class; } // mixed can only be used as a standalone type diff --git a/src/Builder/Aggregation.php b/src/Builder/Aggregation.php index 357e7c709..cb7393e0b 100644 --- a/src/Builder/Aggregation.php +++ b/src/Builder/Aggregation.php @@ -49,14 +49,14 @@ public static function eq(mixed $expression1, mixed $expression2): EqAggregation /** * @param BSONArray|PackedArray|ResolvesToArray|list $input * @param ResolvesToBool|bool $cond - * @param ResolvesToString|non-empty-string|null $as - * @param Int64|ResolvesToInt|int|null $limit + * @param Optional|ResolvesToString|non-empty-string $as + * @param Int64|Optional|ResolvesToInt|int $limit */ public static function filter( PackedArray|ResolvesToArray|BSONArray|array $input, ResolvesToBool|bool $cond, - ResolvesToString|null|string $as = null, - Int64|ResolvesToInt|int|null $limit = null, + Optional|ResolvesToString|string $as = Optional::Undefined, + Optional|Int64|ResolvesToInt|int $limit = Optional::Undefined, ): FilterAggregation { return new FilterAggregation($input, $cond, $as, $limit); diff --git a/src/Builder/Aggregation/FilterAggregation.php b/src/Builder/Aggregation/FilterAggregation.php index f48842d20..d0d0b12d6 100644 --- a/src/Builder/Aggregation/FilterAggregation.php +++ b/src/Builder/Aggregation/FilterAggregation.php @@ -13,6 +13,7 @@ use MongoDB\Builder\Expression\ResolvesToBool; use MongoDB\Builder\Expression\ResolvesToInt; use MongoDB\Builder\Expression\ResolvesToString; +use MongoDB\Builder\Optional; use MongoDB\Model\BSONArray; class FilterAggregation implements ResolvesToArray @@ -22,20 +23,20 @@ class FilterAggregation implements ResolvesToArray public PackedArray|ResolvesToArray|BSONArray|array $input; public ResolvesToBool|bool $cond; - public ResolvesToString|null|string $as; - public Int64|ResolvesToInt|int|null $limit; + public Optional|ResolvesToString|string $as; + public Optional|Int64|ResolvesToInt|int $limit; /** * @param BSONArray|PackedArray|ResolvesToArray|list $input * @param ResolvesToBool|bool $cond - * @param ResolvesToString|non-empty-string|null $as - * @param Int64|ResolvesToInt|int|null $limit + * @param Optional|ResolvesToString|non-empty-string $as + * @param Int64|Optional|ResolvesToInt|int $limit */ public function __construct( PackedArray|ResolvesToArray|BSONArray|array $input, ResolvesToBool|bool $cond, - ResolvesToString|null|string $as = null, - Int64|ResolvesToInt|int|null $limit = null, + Optional|ResolvesToString|string $as = Optional::Undefined, + Optional|Int64|ResolvesToInt|int $limit = Optional::Undefined, ) { if (\is_array($input) && ! \array_is_list($input)) { throw new \InvalidArgumentException('Expected $input argument to be a list, got an associative array.'); diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index f58ea30d9..df3b46d28 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -141,8 +141,7 @@ private function encodeAsObject(ExpressionInterface|StageInterface $value): stdC foreach (get_object_vars($value) as $key => $val) { /** @var mixed $val */ $val = $this->encodeIfSupported($val); - // @todo check for undefined value vs null - if ($val !== null) { + if ($val !== Optional::Undefined) { $result->{$key} = $val; } } diff --git a/src/Builder/Optional.php b/src/Builder/Optional.php new file mode 100644 index 000000000..f16df092b --- /dev/null +++ b/src/Builder/Optional.php @@ -0,0 +1,15 @@ + [ - null, + [], [], ]; yield 'int limit' => [ - 1, + [1], ['limit' => 1], ]; } From d4e53d4e1c4369cc709f8b85065cff52e3134e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 17:57:21 +0200 Subject: [PATCH 29/31] Partial implementation of --- generator/config/stages.yaml | 3 +- generator/src/OperatorGenerator.php | 9 +++++- src/Builder/BuilderEncoder.php | 5 ++++ src/Builder/Stage.php | 9 ++---- src/Builder/Stage/SortStage.php | 15 +++++----- ...erCodecTest.php => BuilderEncoderTest.php} | 30 ++++++++++++++----- 6 files changed, 48 insertions(+), 23 deletions(-) rename tests/Builder/{BuilderCodecTest.php => BuilderEncoderTest.php} (85%) diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index 7ed4d7919..320fa83d1 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -7,7 +7,8 @@ type: stage args: - name: sortSpecification - type: resolvesToObject + type: number # should be an enum PHPLIB-1269 + isVariadic: true - name: limit type: stage args: diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php index bcd0b26dc..b1571a313 100644 --- a/generator/src/OperatorGenerator.php +++ b/generator/src/OperatorGenerator.php @@ -79,7 +79,14 @@ final protected function generateExpressionTypes(ArgumentDefinition $arg): objec $nativeTypes = []; foreach ((array) $arg->type as $type) { $interface = $this->getExpressionTypeInterface($type); - $nativeTypes = array_merge($nativeTypes, [$interface], $this->expressions[$interface]->types); + $types = $this->expressions[$interface]->types; + + // Add the interface to the allowed types if it is not a scalar + if (! $this->expressions[$interface]->scalar) { + $types = array_merge([$interface], $types); + } + + $nativeTypes = array_merge($nativeTypes, $types); } $docTypes = $nativeTypes = array_unique($nativeTypes); diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php index df3b46d28..61b618b04 100644 --- a/src/Builder/BuilderEncoder.php +++ b/src/Builder/BuilderEncoder.php @@ -9,6 +9,7 @@ use MongoDB\Builder\Query\OrQuery; use MongoDB\Builder\Stage\GroupStage; use MongoDB\Builder\Stage\ProjectStage; +use MongoDB\Builder\Stage\SortStage; use MongoDB\Builder\Stage\StageInterface; use MongoDB\Codec\EncodeIfSupported; use MongoDB\Codec\Encoder; @@ -83,6 +84,10 @@ public function encode($value): stdClass|array|string return $this->wrap($value, $result); } + if ($value instanceof SortStage) { + return $this->wrap($value, (object) $value->sortSpecification); + } + if ($value instanceof OrQuery) { $result = []; foreach ($value->query as $query) { diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index 493aa3f1c..de64201aa 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -6,12 +6,9 @@ namespace MongoDB\Builder; -use MongoDB\BSON\Document; use MongoDB\BSON\Int64; -use MongoDB\BSON\Serializable; use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Expression\ResolvesToInt; -use MongoDB\Builder\Expression\ResolvesToObject; use MongoDB\Builder\Stage\GroupStage; use MongoDB\Builder\Stage\LimitStage; use MongoDB\Builder\Stage\MatchStage; @@ -54,11 +51,11 @@ public static function project(mixed ...$specifications): ProjectStage } /** - * @param Document|ResolvesToObject|Serializable|array|object $sortSpecification + * @param int ...$sortSpecification */ - public static function sort(array|object $sortSpecification): SortStage + public static function sort(int ...$sortSpecification): SortStage { - return new SortStage($sortSpecification); + return new SortStage(...$sortSpecification); } /** diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php index 1448debec..254b50657 100644 --- a/src/Builder/Stage/SortStage.php +++ b/src/Builder/Stage/SortStage.php @@ -6,22 +6,23 @@ namespace MongoDB\Builder\Stage; -use MongoDB\BSON\Document; -use MongoDB\BSON\Serializable; -use MongoDB\Builder\Expression\ResolvesToObject; - class SortStage implements StageInterface { public const NAME = '$sort'; public const ENCODE = 'single'; - public array|object $sortSpecification; + /** @param list ...$sortSpecification */ + public array $sortSpecification; /** - * @param Document|ResolvesToObject|Serializable|array|object $sortSpecification + * @param int $sortSpecification */ - public function __construct(array|object $sortSpecification) + public function __construct(int ...$sortSpecification) { + if (\count($sortSpecification) < 1) { + throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', 1, \count($sortSpecification))); + } + $this->sortSpecification = $sortSpecification; } } diff --git a/tests/Builder/BuilderCodecTest.php b/tests/Builder/BuilderEncoderTest.php similarity index 85% rename from tests/Builder/BuilderCodecTest.php rename to tests/Builder/BuilderEncoderTest.php index 64c3e44c9..224a19631 100644 --- a/tests/Builder/BuilderCodecTest.php +++ b/tests/Builder/BuilderEncoderTest.php @@ -20,7 +20,7 @@ * @todo This annotation is not enough as this PHP file needs to use named arguments, that can't compile on PHP 7.4 * @requires PHP 8.0 */ -class BuilderCodecTest extends TestCase +class BuilderEncoderTest extends TestCase { /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#equality-match */ public function testPipeline(): void @@ -39,6 +39,20 @@ public function testPipeline(): void $this->assertSamePipeline($expected, $pipeline); } + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/#ascending-descending-sort */ + public function testSort(): void + { + $pipeline = new Pipeline( + Stage::sort(...['age' => -1, 'posts' => 1]), + ); + + $expected = [ + ['$sort' => ['age' => -1, 'posts' => 1]], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#perform-a-count */ public function testPerformCount(): void { @@ -47,10 +61,10 @@ public function testPerformCount(): void ['score' => [Query::gt(70), Query::lt(90)]], ['views' => Query::gte(1000)], )), - Stage::group( - _id: null, - count: Aggregation::sum(1), - ), + Stage::group(...[ + '_id' => null, + 'count' => Aggregation::sum(1), + ]), ); $expected = [ @@ -80,14 +94,14 @@ public function testPerformCount(): void public function testAggregationFilter(array $limit, array $expectedLimit): void { $pipeline = new Pipeline( - Stage::project( - items: Aggregation::filter( + Stage::project(...[ + 'items' => Aggregation::filter( Expression::arrayFieldPath('items'), Aggregation::gte(Expression::variable('item.price'), 100), 'item', ...$limit, ), - ), + ]), ); $expected = [ From c789074a3edfa653aa5c1c8b4b55053c27fc1f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 2 Oct 2023 21:37:11 +0200 Subject: [PATCH 30/31] Fix type for $limit --- generator/config/expressions.php | 5 +++-- generator/config/stages.yaml | 3 ++- src/Builder/Stage.php | 9 ++++----- src/Builder/Stage/LimitStage.php | 7 +++---- src/Builder/Stage/SortStage.php | 8 +++++--- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/generator/config/expressions.php b/generator/config/expressions.php index 916fa9d5d..5e1283e6f 100644 --- a/generator/config/expressions.php +++ b/generator/config/expressions.php @@ -21,8 +21,9 @@ function typeFieldPath(string $resolvesTo): array return [ 'null' => ['scalar' => true, 'types' => ['null']], - 'number' => ['scalar' => true, 'types' => ['int']], - 'decimal' => ['scalar' => true, 'types' => ['float']], + 'int' => ['scalar' => true, 'types' => ['int', BSON\Int64::class]], + 'number' => ['scalar' => true, 'types' => ['int', BSON\Int64::class]], + 'decimal' => ['scalar' => true, 'types' => ['float', BSON\Decimal128::class]], 'string' => ['scalar' => true, 'types' => ['string']], 'boolean' => ['scalar' => true, 'types' => ['bool']], diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index 320fa83d1..eb9fe7250 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -6,6 +6,7 @@ - name: sort type: stage args: + # @todo should generate be a map, not a list - name: sortSpecification type: number # should be an enum PHPLIB-1269 isVariadic: true @@ -13,7 +14,7 @@ type: stage args: - name: limit - type: resolvesToInt + type: int - name: group type: stage encode: object diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index de64201aa..c1471211f 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -8,7 +8,6 @@ use MongoDB\BSON\Int64; use MongoDB\Builder\Expression\ExpressionInterface; -use MongoDB\Builder\Expression\ResolvesToInt; use MongoDB\Builder\Stage\GroupStage; use MongoDB\Builder\Stage\LimitStage; use MongoDB\Builder\Stage\MatchStage; @@ -27,9 +26,9 @@ public static function group(mixed $_id, mixed ...$fields): GroupStage } /** - * @param Int64|ResolvesToInt|int $limit + * @param Int64|int $limit */ - public static function limit(Int64|ResolvesToInt|int $limit): LimitStage + public static function limit(Int64|int $limit): LimitStage { return new LimitStage($limit); } @@ -51,9 +50,9 @@ public static function project(mixed ...$specifications): ProjectStage } /** - * @param int ...$sortSpecification + * @param Int64|int ...$sortSpecification */ - public static function sort(int ...$sortSpecification): SortStage + public static function sort(Int64|int ...$sortSpecification): SortStage { return new SortStage(...$sortSpecification); } diff --git a/src/Builder/Stage/LimitStage.php b/src/Builder/Stage/LimitStage.php index e8ed074f8..78d1dd4c7 100644 --- a/src/Builder/Stage/LimitStage.php +++ b/src/Builder/Stage/LimitStage.php @@ -7,19 +7,18 @@ namespace MongoDB\Builder\Stage; use MongoDB\BSON\Int64; -use MongoDB\Builder\Expression\ResolvesToInt; class LimitStage implements StageInterface { public const NAME = '$limit'; public const ENCODE = 'single'; - public Int64|ResolvesToInt|int $limit; + public Int64|int $limit; /** - * @param Int64|ResolvesToInt|int $limit + * @param Int64|int $limit */ - public function __construct(Int64|ResolvesToInt|int $limit) + public function __construct(Int64|int $limit) { $this->limit = $limit; } diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php index 254b50657..b06177df4 100644 --- a/src/Builder/Stage/SortStage.php +++ b/src/Builder/Stage/SortStage.php @@ -6,18 +6,20 @@ namespace MongoDB\Builder\Stage; +use MongoDB\BSON\Int64; + class SortStage implements StageInterface { public const NAME = '$sort'; public const ENCODE = 'single'; - /** @param list ...$sortSpecification */ + /** @param list ...$sortSpecification */ public array $sortSpecification; /** - * @param int $sortSpecification + * @param Int64|int $sortSpecification */ - public function __construct(int ...$sortSpecification) + public function __construct(Int64|int ...$sortSpecification) { if (\count($sortSpecification) < 1) { throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', 1, \count($sortSpecification))); From 5bf47537028684b7fb5697de97ad32b82ca33229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 4 Oct 2023 12:18:47 +0200 Subject: [PATCH 31/31] Add object and double to type system --- generator/config/expressions.php | 6 ++++-- generator/config/stages.yaml | 2 +- src/Builder/Stage.php | 6 ++++-- src/Builder/Stage/SortStage.php | 8 +++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/generator/config/expressions.php b/generator/config/expressions.php index 5e1283e6f..5e8a6b5d3 100644 --- a/generator/config/expressions.php +++ b/generator/config/expressions.php @@ -22,10 +22,12 @@ function typeFieldPath(string $resolvesTo): array return [ 'null' => ['scalar' => true, 'types' => ['null']], 'int' => ['scalar' => true, 'types' => ['int', BSON\Int64::class]], - 'number' => ['scalar' => true, 'types' => ['int', BSON\Int64::class]], - 'decimal' => ['scalar' => true, 'types' => ['float', BSON\Decimal128::class]], + 'double' => ['scalar' => true, 'types' => ['int', BSON\Int64::class, 'float']], + 'decimal' => ['scalar' => true, 'types' => ['int', BSON\Int64::class, 'float', BSON\Decimal128::class]], + 'number' => ['scalar' => true, 'types' => ['int', BSON\Int64::class, 'float', BSON\Decimal128::class]], 'string' => ['scalar' => true, 'types' => ['string']], 'boolean' => ['scalar' => true, 'types' => ['bool']], + 'object' => ['scalar' => true, 'types' => ['array', 'object', BSON\Document::class, BSON\Serializable::class]], // Use Interface suffix to avoid confusion with MongoDB\Builder\Expression factory class ExpressionInterface::class => [ diff --git a/generator/config/stages.yaml b/generator/config/stages.yaml index eb9fe7250..b973c6db2 100644 --- a/generator/config/stages.yaml +++ b/generator/config/stages.yaml @@ -8,7 +8,7 @@ args: # @todo should generate be a map, not a list - name: sortSpecification - type: number # should be an enum PHPLIB-1269 + type: [int, object] # should be an enum PHPLIB-1269 isVariadic: true - name: limit type: stage diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php index c1471211f..7466925bd 100644 --- a/src/Builder/Stage.php +++ b/src/Builder/Stage.php @@ -6,7 +6,9 @@ namespace MongoDB\Builder; +use MongoDB\BSON\Document; use MongoDB\BSON\Int64; +use MongoDB\BSON\Serializable; use MongoDB\Builder\Expression\ExpressionInterface; use MongoDB\Builder\Stage\GroupStage; use MongoDB\Builder\Stage\LimitStage; @@ -50,9 +52,9 @@ public static function project(mixed ...$specifications): ProjectStage } /** - * @param Int64|int ...$sortSpecification + * @param Document|Int64|Serializable|array|int|object ...$sortSpecification */ - public static function sort(Int64|int ...$sortSpecification): SortStage + public static function sort(array|int|object ...$sortSpecification): SortStage { return new SortStage(...$sortSpecification); } diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php index b06177df4..814d5cf3c 100644 --- a/src/Builder/Stage/SortStage.php +++ b/src/Builder/Stage/SortStage.php @@ -6,20 +6,22 @@ namespace MongoDB\Builder\Stage; +use MongoDB\BSON\Document; use MongoDB\BSON\Int64; +use MongoDB\BSON\Serializable; class SortStage implements StageInterface { public const NAME = '$sort'; public const ENCODE = 'single'; - /** @param list ...$sortSpecification */ + /** @param list ...$sortSpecification */ public array $sortSpecification; /** - * @param Int64|int $sortSpecification + * @param Document|Int64|Serializable|array|int|object $sortSpecification */ - public function __construct(Int64|int ...$sortSpecification) + public function __construct(array|int|object ...$sortSpecification) { if (\count($sortSpecification) < 1) { throw new \InvalidArgumentException(\sprintf('Expected at least %d values, got %d.', 1, \count($sortSpecification)));