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;