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 all 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
132 changes: 132 additions & 0 deletions generator/src/FluentStageFactoryGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

namespace MongoDB\CodeGenerator;

use MongoDB\Builder\Pipeline;
use MongoDB\Builder\Stage;
use MongoDB\Builder\Type\StageInterface;
use MongoDB\CodeGenerator\Definition\GeneratorDefinition;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\Parameter;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PhpNamespace;
use Nette\PhpGenerator\TraitType;
use ReflectionClass;

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

/**
* Generates a fluent factory trait for aggregation pipeline stages.
* The method definition is based on all the public static methods
* of the Stage class.
*/
class FluentStageFactoryGenerator extends OperatorGenerator
{
private const FACTORY_CLASS = Stage::class;

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

private function createFluentFactoryTrait(GeneratorDefinition $definition): PhpNamespace
{
$namespace = new PhpNamespace($definition->namespace);
$trait = $namespace->addTrait('FluentFactoryTrait');

$namespace->addUse(self::FACTORY_CLASS);
$namespace->addUse(StageInterface::class);
$namespace->addUse(Pipeline::class);

$trait->addProperty('pipeline')
->setType('array')
->setComment('@var list<StageInterface>')
->setValue([]);
$trait->addMethod('getPipeline')
->setReturnType(Pipeline::class)
->setBody(<<<'PHP'
return new Pipeline(...$this->pipeline);
PHP);

$this->addUsesFrom(self::FACTORY_CLASS, $namespace);
$staticFactory = ClassType::from(self::FACTORY_CLASS);
assert($staticFactory instanceof ClassType);

// Import the methods customized in the factory class
foreach ($staticFactory->getMethods() as $method) {
$this->addMethod($method, $trait);
}

// Import the other methods provided by the generated trait
foreach ($staticFactory->getTraits() as $usedTrait) {
$this->addUsesFrom($usedTrait->getName(), $namespace);
$staticFactory = TraitType::from($usedTrait->getName());
assert($staticFactory instanceof TraitType);
foreach ($staticFactory->getMethods() as $method) {
$this->addMethod($method, $trait);
}
}

return $namespace;
}

private function addMethod(Method $factoryMethod, TraitType $trait): void
{
// Non-public methods are not part of the API
if (! $factoryMethod->isPublic()) {
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. 👍

}

// Some methods can be overridden in the class, so we skip them
// when importing the methods provided by the trait.
if ($trait->hasMethod($factoryMethod->getName())) {
return;
}

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

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

$args = array_map(
fn (Parameter $param): string => '$' . $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,
(new ReflectionClass(self::FACTORY_CLASS))->getShortName(),
$factoryMethod->getName(),
implode(', ', $args),
));
}

private static function addUsesFrom(string $classLike, PhpNamespace $namespace): void
{
$file = PhpFile::fromCode(file_get_contents((new ReflectionClass($classLike))->getFileName()));

foreach ($file->getNamespaces() as $ns) {
foreach ($ns->getUses() as $use) {
$namespace->addUse($use);
}
}
}
}
Loading