Skip to content
This repository was archived by the owner on Feb 28, 2025. It is now read-only.

Add fluent builder for Laravel #67

Merged
merged 5 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions generator/config/definitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace MongoDB\CodeGenerator\Config;

use MongoDB\CodeGenerator\FluentStageFactoryGenerator;
use MongoDB\CodeGenerator\OperatorClassGenerator;
use MongoDB\CodeGenerator\OperatorFactoryGenerator;
use MongoDB\CodeGenerator\OperatorTestGenerator;
Expand All @@ -18,6 +19,7 @@
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
OperatorTestGenerator::class,
FluentStageFactoryGenerator::class,
],
],

Expand Down
163 changes: 163 additions & 0 deletions generator/src/FluentStageFactoryGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);

namespace MongoDB\CodeGenerator;

use MongoDB\BSON\Decimal128;
use MongoDB\BSON\Document;
use MongoDB\BSON\Int64;
use MongoDB\BSON\PackedArray;
use MongoDB\BSON\Serializable;
use MongoDB\BSON\Timestamp;
use MongoDB\BSON\Type;
use MongoDB\Builder\Expression\ArrayFieldPath;
use MongoDB\Builder\Expression\FieldPath;
use MongoDB\Builder\Expression\ResolvesToArray;
use MongoDB\Builder\Expression\ResolvesToObject;
use MongoDB\Builder\Pipeline;
use MongoDB\Builder\Stage;
use MongoDB\Builder\Type\AccumulatorInterface;
use MongoDB\Builder\Type\ExpressionInterface;
use MongoDB\Builder\Type\FieldQueryInterface;
use MongoDB\Builder\Type\Optional;
use MongoDB\Builder\Type\QueryInterface;
use MongoDB\Builder\Type\Sort;
use MongoDB\Builder\Type\StageInterface;
use MongoDB\CodeGenerator\Definition\GeneratorDefinition;
use MongoDB\Model\BSONArray;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\PhpNamespace;
use Nette\PhpGenerator\TraitType;
use RuntimeException;
use stdClass;
use Throwable;

use function array_key_last;
use function array_map;
use function assert;
use function implode;
use function sprintf;

/**
* Generates a fluent factory class for aggregation pipeline stages.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Generates a fluent factory class for aggregation pipeline stages.
* Generates a fluent factory trait for aggregation pipeline stages.

Make a note to update other references of "class" to "trait" below. For instance, createFluentFactoryClass() should now be createFluentFactoryTrait().

This very class might also be better named FluentFactoryTraitGenerator().

Copy link
Member Author

Choose a reason for hiding this comment

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

I want to keep "Stage" in the generator class name.

Copy link
Member

Choose a reason for hiding this comment

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

Should the generated file be FluentStageFactoryTrait.php in that case? I'll defer to you, as I'm not sure if you have future plans for more traits.

Copy link
Member Author

Choose a reason for hiding this comment

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

The file is in the Stage namespace.

* The method definition is based on the manually edited static class
* that imports the stage factory trait.
*/
class FluentStageFactoryGenerator extends OperatorGenerator
{
/**
* All public of this class are duplicated as instance methods of the
* fluent factory class.
*/
private const FACTORY_CLASS = Stage::class;

public function generate(GeneratorDefinition $definition): void
{
$this->writeFile($this->createFluentFactoryClass($definition));
}

private function createFluentFactoryClass(GeneratorDefinition $definition): PhpNamespace
{
$namespace = new PhpNamespace($definition->namespace);
$class = $namespace->addClass('FluentFactory');

$namespace->addUse(StageInterface::class);
$namespace->addUse(FieldQueryInterface::class);
$namespace->addUse(Pipeline::class);
$namespace->addUse(Decimal128::class);
$namespace->addUse(Document::class);
$namespace->addUse(Int64::class);
$namespace->addUse(PackedArray::class);
$namespace->addUse(Serializable::class);
$namespace->addUse(Timestamp::class);
$namespace->addUse(Type::class);
$namespace->addUse(ArrayFieldPath::class);
$namespace->addUse(FieldPath::class);
$namespace->addUse(ResolvesToArray::class);
$namespace->addUse(ResolvesToObject::class);
$namespace->addUse(Pipeline::class);
$namespace->addUse(AccumulatorInterface::class);
$namespace->addUse(ExpressionInterface::class);
$namespace->addUse(Optional::class);
$namespace->addUse(QueryInterface::class);
$namespace->addUse(Sort::class);
$namespace->addUse(BSONArray::class);
$namespace->addUse(stdClass::class);
$namespace->addUse(self::FACTORY_CLASS);
$class->addProperty('pipeline')
->setType('array')
->setComment('@var list<StageInterface>')
->setValue([]);
$class->addMethod('getPipeline')
->setReturnType(Pipeline::class)
->setBody(<<<'PHP'
return new Pipeline(...$this->pipeline);
PHP);

$staticFactory = ClassType::from(self::FACTORY_CLASS);
assert($staticFactory instanceof ClassType);
foreach ($staticFactory->getMethods() as $method) {
if (! $method->isPublic()) {
continue;
}

try {
$this->addMethod($method, $namespace, $class);
} catch (Throwable $e) {
throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e);
}
}

$staticFactory = TraitType::from(Stage\FactoryTrait::class);
assert($staticFactory instanceof TraitType);
foreach ($staticFactory->getMethods() as $method) {
if (! $method->isPublic()) {
continue;
}

try {
$this->addMethod($method, $namespace, $class);
} catch (Throwable $e) {
throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e);
}
}

return $namespace;
}

private function addMethod(Method $factoryMethod, PhpNamespace $namespace, ClassType $class): void
{
if ($class->hasMethod($factoryMethod->getName())) {
return;
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to throw here? What is the legitimate case where the method already exists and you'd want to NOP?

Copy link
Member Author

Choose a reason for hiding this comment

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

The method match is redefined in the Stage class. The generated version from the trait must be ignored.

Copy link
Member

Choose a reason for hiding this comment

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

Noted. In this case, a comment would be helpful to clarify that you're giving preference to the redefined Stage methods instead of those from the trait. And this assumes you add the Stage methods before the trait (no need to repeat that, it's clear from the order of addMethod() calls in the generation method).

Copy link
Member Author

Choose a reason for hiding this comment

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

That's essentially what I said in the comment above.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, my mistake. I just realized this entire thread was incorrectly applied to the early return for non-public methods.

I see the comment below for skipping overridden methods. 👍

}

$method = $class->addMethod($factoryMethod->getName());

$method->setComment($factoryMethod->getComment());
$method->setParameters($factoryMethod->getParameters());

$args = array_map(
fn ($param) => '$' . $param->getName(),
$factoryMethod->getParameters(),
);

if ($factoryMethod->isVariadic()) {
$method->setVariadic();
$args[array_key_last($args)] = '...' . $args[array_key_last($args)];
}

$method->setReturnType('static');
$method->setBody(sprintf(
<<<'PHP'
$this->pipeline[] = %s::%s(%s);

return $this;
PHP,
'Stage',
$factoryMethod->getName(),
implode(',', $args),
));
}
}
Loading