From ff20d12bdccf554b44f8c177b28da0a054ce491d Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 18 Apr 2025 22:11:07 +0200 Subject: [PATCH 1/6] Fixed the repository build handler so that it uses the corresponding db fields mappings from the Column annotation instead of relying on the Mapping annotation --- .github/workflows/ci.yml | 2 +- .github/workflows/psalm.yml | 2 +- .gitignore | 2 + composer.json | 2 +- src/ClassBuilder/ClassBuilder.php | 2 +- src/Collection/CollectionInterface.php | 44 ++++-- src/Collection/Klist.php | 23 +++ src/Collection/Kmap.php | 36 ++++- src/Repository/Handler/ParsedMethodPart.php | 24 +++- .../Handler/RepositoryBuildHandler.php | 132 +++++++----------- src/Storage/Criteria/Operator.php | 46 +++--- src/Storage/Entity/EntityMetadataService.php | 5 +- 12 files changed, 203 insertions(+), 117 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2386f31..5d68aae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' coverage: xdebug - name: Install dependencies diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 8036e13..85a26e5 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -16,7 +16,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' - name: Install dependencies run: composer install --prefer-dist --no-progress diff --git a/.gitignore b/.gitignore index 089088d..5afbeb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea +.DS_Store +dev.php vendor composer.lock .phpunit.result.cache \ No newline at end of file diff --git a/composer.json b/composer.json index 7847bc4..8125984 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type": "library", "version": "1.0.2", "require": { - "php": ">=8.1", + "php": ">=8.3", "ext-mysqli": "*", "ext-pdo": "*" }, diff --git a/src/ClassBuilder/ClassBuilder.php b/src/ClassBuilder/ClassBuilder.php index bc1b379..ee36271 100644 --- a/src/ClassBuilder/ClassBuilder.php +++ b/src/ClassBuilder/ClassBuilder.php @@ -79,7 +79,7 @@ public function build( string $class ): string { */ private function generateProxyClass( string $class, BuildContext $buildOutput ): string { // Generate a unique proxy class name by replacing backslashes in the class name. - $proxiedClassName = str_replace( "\\", '_', $class ); + $proxiedClassName = str_replace( "\\", '_', $class ) . 'Proxy'; // Define whether the proxy class extends or implements the original class. $inheritanceType = $this->reflect->isInterface( $class ) ? 'implements' : 'extends'; diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php index 75d4599..d817109 100644 --- a/src/Collection/CollectionInterface.php +++ b/src/Collection/CollectionInterface.php @@ -46,9 +46,10 @@ public function isNotEmpty(): bool; * Filter the collection based on a predicate. * * @param Closure(TValue):bool $predicate + * * @return static */ - public function filter(Closure $predicate): static; + public function filter( Closure $predicate ): static; /** * Filter out null values from the collection. @@ -62,25 +63,28 @@ public function filterNotNull(): static; * * @template TOut * @param Closure(TValue):TOut $transform + * * @return CollectionInterface */ - public function map(Closure $transform): CollectionInterface; + public function map( Closure $transform ): CollectionInterface; /** * Execute a function for each element in the collection. * * @param Closure(TValue):void $transform + * * @return static */ - public function foreach(Closure $transform): static; + public function foreach( Closure $transform ): static; /** * Determine if any element in the collection satisfies a predicate. * * @param Closure(TValue):bool $predicate + * * @return bool */ - public function any(Closure $predicate): bool; + public function any( Closure $predicate ): bool; /** * Retrieve the first element in the collection or null if empty. @@ -93,25 +97,28 @@ public function firstOrNull(): mixed; * Determine if all elements in the collection satisfy a predicate. * * @param Closure(TValue):bool $predicate + * * @return bool */ - public function all(Closure $predicate): bool; + public function all( Closure $predicate ): bool; /** * Merge the collection with another array. * * @param array $array + * * @return static */ - public function mergeArray(array $array): static; + public function mergeArray( array $array ): static; /** * Merge the collection with another collection. * * @param CollectionInterface $collection + * * @return static */ - public function merge(CollectionInterface $collection): static; + public function merge( CollectionInterface $collection ): static; /** * Join elements of the collection into a string with a separator. @@ -120,7 +127,7 @@ public function merge(CollectionInterface $collection): static; * * @return string */ - public function join(string $separator): string; + public function join( string $separator ): string; /** * Apply a predicate if the collection is not empty. @@ -131,7 +138,7 @@ public function join(string $separator): string; * * @return static */ - public function maybe(Closure $predicate): static; + public function maybe( Closure $predicate ): static; /** * Convert the collection to a mutable variant. @@ -157,4 +164,23 @@ public function toImmutable(): static; * @return static */ public function flatten(): static; + + /** + * Reduce the collection to a single value using a callback. + * + * @psalm-suppress PossiblyUnusedMethod + * + * @param Closure(mixed, TValue):mixed $transform + * @param mixed|null $initial + * + * @return mixed + */ + public function reduce( Closure $transform, mixed $initial = null ): mixed; + + /** + * Returns the current item and advances the internal pointer. + * + * @return TValue|null + */ + public function nextAndGet(): mixed; } diff --git a/src/Collection/Klist.php b/src/Collection/Klist.php index 13e3e78..5b61a11 100644 --- a/src/Collection/Klist.php +++ b/src/Collection/Klist.php @@ -170,6 +170,16 @@ public function next(): void { $this->internalMap->next(); } + /** + * Returns the current item and advances the internal pointer. + * + * @return TValue|null + */ + #[Override] + public function nextAndGet(): mixed { + return $this->internalMap->nextAndGet(); + } + #[Override] public function key(): mixed { return $this->internalMap->key(); @@ -257,4 +267,17 @@ public function firstOrNull(): mixed { public function mapOf( Closure $transform ): KMap { return $this->internalMap->map( fn( $key, $value ) => $transform( $value ) ); } + + /** + * Reduce the collection to a single value. + * + * @param Closure(mixed, TValue):mixed $transform + * @param mixed|null $initial + * + * @return mixed + */ + #[Override] + public function reduce( Closure $transform, mixed $initial = null ): mixed { + return $this->internalMap->reduce( $transform, $initial ); + } } diff --git a/src/Collection/Kmap.php b/src/Collection/Kmap.php index ecaf4e1..635d7fe 100644 --- a/src/Collection/Kmap.php +++ b/src/Collection/Kmap.php @@ -24,7 +24,7 @@ class Kmap implements CollectionInterface { /** * @param array $array - * @param bool $mutable + * @param bool $mutable */ public function __construct( array $array = [], private readonly bool $mutable = false ) { $this->keys = array_keys( $array ); @@ -46,6 +46,19 @@ public function next(): void { $this->index ++; } + /** + * Returns the current item and advances the internal pointer. + * + * @return TValue|null + */ + #[Override] + public function nextAndGet(): mixed { + $current = $this->current(); + $this->next(); + + return $current; + } + #[Override] public function valid(): bool { return isset( $this->keys[ $this->index ] ) && array_key_exists( $this->keys[ $this->index ], $this->array ); @@ -216,6 +229,7 @@ public function merge( CollectionInterface $collection ): static { * @template TMapKey of array-key * @template TMapValue * @param Closure(TKey, TValue): array $transform + * * @return Kmap */ #[Override] @@ -329,7 +343,7 @@ public function firstOrNull(): mixed { } /** - * @param TKey $key + * @param TKey $key * @param TValue $element * * @return void @@ -348,4 +362,22 @@ public function resetKeys(): void { $this->array = array_values( $this->array ); $this->keys = array_keys( $this->array ); } + + /** + * Reduce the collection to a single value. + * + * @param Closure(mixed, TValue):mixed $transform + * @param mixed|null $initial + * + * @return mixed + */ + #[Override] + public function reduce( Closure $transform, mixed $initial = null ): mixed { + $carry = $initial; + foreach ( $this->array as $element ) { + $carry = $transform( $carry, $element ); + } + + return $carry; + } } diff --git a/src/Repository/Handler/ParsedMethodPart.php b/src/Repository/Handler/ParsedMethodPart.php index 4e1922a..dfab117 100644 --- a/src/Repository/Handler/ParsedMethodPart.php +++ b/src/Repository/Handler/ParsedMethodPart.php @@ -8,12 +8,26 @@ /** * @psalm-suppress PossiblyUnusedProperty */ -class ParsedMethodPart { +readonly class ParsedMethodPart { public function __construct( - public readonly Prefix $prefix, - public readonly LogicOperator $logicOperator, - public readonly string $field, - public readonly Operator $operator, + public Prefix $prefix, + public LogicOperator $logicOperator, + public string $field, + public Operator $operator, ) { } + + public function copy( + ?Prefix $prefix = null, + ?LogicOperator $logicOperator = null, + ?string $field = null, + ?Operator $operator = null, + ): self { + return new self( + prefix: $prefix ?? $this->prefix, + logicOperator: $logicOperator ?? $this->logicOperator, + field: $field ?? $this->field, + operator: $operator ?? $this->operator, + ); + } } \ No newline at end of file diff --git a/src/Repository/Handler/RepositoryBuildHandler.php b/src/Repository/Handler/RepositoryBuildHandler.php index 6305774..0501038 100644 --- a/src/Repository/Handler/RepositoryBuildHandler.php +++ b/src/Repository/Handler/RepositoryBuildHandler.php @@ -5,34 +5,44 @@ use Axpecto\Annotation\Annotation; use Axpecto\ClassBuilder\BuildContext; use Axpecto\ClassBuilder\BuildHandler; +use Axpecto\Collection\Klist; use Axpecto\Reflection\ReflectionUtils; use Axpecto\Repository\Mapper\ArrayToEntityMapper; use Axpecto\Repository\Repository; use Axpecto\Storage\Criteria\Operator; use Axpecto\Storage\Entity\Entity as EntityAttribute; -use Axpecto\Storage\Entity\Mapping; +use Axpecto\Storage\Entity\EntityField; +use Axpecto\Storage\Entity\EntityMetadataService; use Exception; +use InvalidArgumentException; use Override; +use ReflectionException; use ReflectionMethod; +use ReflectionParameter; -class RepositoryBuildHandler implements BuildHandler { +readonly class RepositoryBuildHandler implements BuildHandler { /** * @psalm-suppress PossiblyUnusedMethod * - * @param ReflectionUtils $reflectUtils + * @param ReflectionUtils $reflectUtils * @param RepositoryMethodNameParser $nameParser + * @param EntityMetadataService $metadataService */ public function __construct( - private readonly ReflectionUtils $reflectUtils, - private readonly RepositoryMethodNameParser $nameParser, + private ReflectionUtils $reflectUtils, + private RepositoryMethodNameParser $nameParser, + private EntityMetadataService $metadataService, ) { } + /** + * @throws ReflectionException + */ #[Override] public function intercept( Annotation $annotation, BuildContext $context ): void { if ( ! $annotation instanceof Repository || $annotation->getAnnotatedMethod() !== null ) { - return; + throw new InvalidArgumentException( 'Invalid annotation type or method.' ); } /** @var EntityAttribute $entityAnnotation */ @@ -55,9 +65,9 @@ public function intercept( Annotation $annotation, BuildContext $context ): void * and returns the storage call result. * * @param ReflectionMethod $method - * @param BuildContext $output - * @param Repository $repositoryAnnotation - * @param EntityAttribute $entityAnnotation + * @param BuildContext $output + * @param Repository $repositoryAnnotation + * @param EntityAttribute $entityAnnotation * * @throws Exception * @@ -75,90 +85,54 @@ protected function implementAbstractMethod( $mapperReference = $output->injectProperty( 'mapper', ArrayToEntityMapper::class ); $storageReference = $output->injectProperty( 'storage', $entityAnnotation->storage ); - // Build an associative map of entity property => database field using the Mapping annotation. - $entityFieldMapping = $this->reflectUtils - ->getConstructorArguments( $entityClass ) - ->mapOf( function ( $arg ) use ( $entityClass ) { - $mapping = $this->reflectUtils - ->getParamAnnotations( $entityClass, '__construct', $arg->name, Mapping::class ) - ->firstOrNull(); - - return [ $arg->name => $mapping ? $mapping->fromField : $arg->name ]; - } ); + // Build an associative map of field names to their database field names. + $fields = $this->metadataService + ->getFields( $entityClass ) + ->mapOf( fn( EntityField $field ) => [ $field->name => $field->persistenceMapping ] ); // Parse the method name into parts (ParsedMethodPart instances). - $methodParts = $this->nameParser->parse( $method->getName() ); - $methodName = $method->getName(); - $methodSignature = $this->reflectUtils->getMethodDefinitionString( $method->class, $methodName ); + $methodParts = $this->nameParser->parse( $method->getName() ); // Calculate the expected argument count from the parsed method parts. - $expectedCount = 0; - foreach ( $methodParts->toArray() as $part ) { - if ( empty( $part->field ) ) { - continue; - } - if ( $part->operator === Operator::BETWEEN ) { - $expectedCount += 2; - } elseif ( $part->operator === Operator::IS_NULL || $part->operator === Operator::IS_NOT_NULL ) { - // No argument required. - } else { - $expectedCount ++; - } - } + $expectedCount = $methodParts->reduce( fn( $count, ParsedMethodPart $part ) => $count + $part->operator->argumentCount(), 0 ); // Compare with the declared parameter count of the method. - $declaredParameters = $method->getParameters(); - $declaredCount = count( $declaredParameters ); + $declaredParameters = listFrom( $method->getParameters() ); + $declaredCount = $declaredParameters->count(); if ( $declaredCount !== $expectedCount ) { - throw new \Exception( "Method {$method->getName()} declares {$declaredCount} arguments, but parsed conditions require {$expectedCount}." ); + throw new Exception( "Method {$method->getName()} declares $declaredCount arguments, but parsed conditions require $expectedCount." ); } - // Get declared parameter names (as strings with the '$' prefix). - $paramNames = array_map( fn( $p ) => '$' . $p->getName(), $declaredParameters ); - - // Define indentation: 2 tabs. - $indent = "\t\t"; - - // Start building the generated code. - $code = "\$criteria = new \\Axpecto\\Storage\\Criteria\\Criteria();\n"; - $i = 0; - - // Iterate over each parsed method part using Klist->foreach. - $methodParts->foreach( function ( ParsedMethodPart $part ) use ( &$code, &$paramNames, &$i, $entityClass, $entityFieldMapping, $indent, $output ) { - // If no field is provided, skip this part. - if ( empty( $part->field ) ) { - return; - } - // Verify that the entity defines this property. - if ( ! isset( $entityFieldMapping[ $part->field ] ) ) { - throw new \Exception( "Error building repository {$output->class}. Field '{$part->field}' is not defined in entity '{$entityClass}'." ); - } - // Use the mapped field name. - $dbField = $entityFieldMapping[ $part->field ]; - - // Generate condition code using the declared parameter names. - if ( $part->operator === Operator::BETWEEN ) { - // BETWEEN requires two parameters. - $code .= $indent . "\$criteria->addCondition('{$dbField}', [{$paramNames[$i]}, {$paramNames[$i+1]}], \\Axpecto\\Storage\\Criteria\\Operator::BETWEEN, \\Axpecto\\Storage\\Criteria\\LogicOperator::{$part->logicOperator->name});\n"; - $i += 2; - } elseif ( $part->operator === Operator::IS_NULL || $part->operator === Operator::IS_NOT_NULL ) { - // Operators that require no argument. - $code .= $indent . "\$criteria->addCondition('{$dbField}', null, \\Axpecto\\Storage\\Criteria\\Operator::{$part->operator->name}, \\Axpecto\\Storage\\Criteria\\LogicOperator::{$part->logicOperator->name});\n"; - } else { - // Otherwise, assume one argument is required. - $code .= $indent . "\$criteria->addCondition('{$dbField}', {$paramNames[$i]}, \\Axpecto\\Storage\\Criteria\\Operator::{$part->operator->name}, \\Axpecto\\Storage\\Criteria\\LogicOperator::{$part->logicOperator->name});\n"; - $i ++; - } - } ); + // Create a list of parameter names for the method. + $paramNames = $declaredParameters->map( fn( ReflectionParameter $p ) => $p->name ); + + // Filter out the method parts that have a field defined and map with the database field names. + $mappedMethodParts = $methodParts + ->filter( fn( ParsedMethodPart $part ) => $part->field ) + ->map( fn( ParsedMethodPart $part ) => $part->copy( field: $fields[ $part->field ] ?? throw new Exception( "Unknown field: $part->field" ) ) ); + + $code = $mappedMethodParts->reduce( + fn( $carry, ParsedMethodPart $part ) => $carry . $this->mapMethodPartToCode( $part, $paramNames ), + initial: "\$criteria = new \\Axpecto\\Storage\\Criteria\\Criteria();\n" + ); // Generate the final storage call. - $code .= $indent . "return \$this->{$storageReference}->findAllByCriteria(\$criteria, '{$entityClass}')\n"; - $code .= $indent . " ->map(fn(\$item) => \$this->{$mapperReference}->map('{$entityClass}', \$item));"; + $code .= "\t\treturn \$this->{$storageReference}->findAllByCriteria(\$criteria, '{$entityClass}')\n"; + $code .= "\t\t ->map(fn(\$item) => \$this->{$mapperReference}->map('{$entityClass}', \$item));"; $output->addMethod( - name: $methodName, - signature: $methodSignature, + name: $method->getName(), + signature: $this->reflectUtils->getMethodDefinitionString( $method->class, $method->getName() ), implementation: $code, ); } + + private function mapMethodPartToCode( ParsedMethodPart $part, Klist $params ): string { + return match ( $part->operator ) { + Operator::BETWEEN => "\t\t\$criteria->addCondition('$part->field', [\${$params->nextAndGet()}, \${$params->nextAndGet()}], \\Axpecto\\Storage\\Criteria\\Operator::BETWEEN, \\Axpecto\\Storage\\Criteria\\LogicOperator::{$part->logicOperator->name});\n", + Operator::IS_NULL, + Operator::IS_NOT_NULL => "\t\t\$criteria->addCondition('$part->field', null, \\Axpecto\\Storage\\Criteria\\Operator::{$part->operator->name}, \\Axpecto\\Storage\\Criteria\\LogicOperator::{$part->logicOperator->name});\n", + default => "\t\t\$criteria->addCondition('$part->field', \${$params->nextAndGet()}, \\Axpecto\\Storage\\Criteria\\Operator::{$part->operator->name}, \\Axpecto\\Storage\\Criteria\\LogicOperator::{$part->logicOperator->name});\n", + }; + } } diff --git a/src/Storage/Criteria/Operator.php b/src/Storage/Criteria/Operator.php index 335c39f..46ce98f 100644 --- a/src/Storage/Criteria/Operator.php +++ b/src/Storage/Criteria/Operator.php @@ -6,24 +6,36 @@ enum Operator: string { case GREATER_THAN_EQUAL = 'GreaterThanEqual'; - case GREATER_THAN = 'GreaterThan'; - case LESS_THAN_EQUAL = 'LessThanEqual'; - case LESS_THAN = 'LessThan'; - case BETWEEN = 'Between'; - case NOT_IN = 'NotIn'; - case IN = 'In'; - case IS_NOT_NULL = 'IsNotNull'; - case IS_NULL = 'IsNull'; - case NOT_LIKE = 'NotLike'; - case STARTING_WITH = 'StartingWith'; - case ENDING_WITH = 'EndingWith'; - case CONTAINS = 'Contains'; - case LIKE = 'Like'; - case BEFORE = 'Before'; - case AFTER = 'After'; - case EQUALS = 'Equals'; + case GREATER_THAN = 'GreaterThan'; + case LESS_THAN_EQUAL = 'LessThanEqual'; + case LESS_THAN = 'LessThan'; + case BETWEEN = 'Between'; + case NOT_IN = 'NotIn'; + case IN = 'In'; + case IS_NOT_NULL = 'IsNotNull'; + case IS_NULL = 'IsNull'; + case NOT_LIKE = 'NotLike'; + case STARTING_WITH = 'StartingWith'; + case ENDING_WITH = 'EndingWith'; + case CONTAINS = 'Contains'; + case LIKE = 'Like'; + case BEFORE = 'Before'; + case AFTER = 'After'; + case EQUALS = 'Equals'; - public static function getList() : Klist { + public static function getList(): Klist { return listFrom( self::cases() ); } + + /** + * Returns the number of arguments this operator requires. + */ + public function argumentCount(): int { + return match ( $this ) { + self::IS_NULL, + self::IS_NOT_NULL => 0, + self::BETWEEN => 2, + default => 1, + }; + } } diff --git a/src/Storage/Entity/EntityMetadataService.php b/src/Storage/Entity/EntityMetadataService.php index e9e6b97..8f103b9 100644 --- a/src/Storage/Entity/EntityMetadataService.php +++ b/src/Storage/Entity/EntityMetadataService.php @@ -55,6 +55,9 @@ public function getEntity( string $entityClass ): Entity { return $entityAnnotation; } + /** + * @throws ReflectionException + */ private function mapArgumentToEntityField( Argument $argument, string $entity ): EntityField { /* @var Column $column */ $column = $this->reflectionUtils->getParamAnnotations( @@ -70,7 +73,7 @@ private function mapArgumentToEntityField( Argument $argument, string $entity ): nullable: $column?->isNullable ?? $argument->nullable, entityClass: $entity, default: $column?->defaultValue ?? $argument->default ?? EntityField::NO_DEFAULT_VALUE_SPECIFIED, - persistenceMapping: $column?->toField ?? $argument->name, + persistenceMapping: $column?->name ?? $argument->name, isAutoIncrement: $column?->autoIncrement ?? false, isPrimary: $column?->isPrimary ?? false, isUnique: $column?->isUnique ?? false, From 08a6a575ba61fd16ae65aa06ca76485bc43e2076 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 18 Apr 2025 22:11:38 +0200 Subject: [PATCH 2/6] Removed the Mapping annotation in favor of the Column annotation --- src/Storage/Entity/Mapping.php | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/Storage/Entity/Mapping.php diff --git a/src/Storage/Entity/Mapping.php b/src/Storage/Entity/Mapping.php deleted file mode 100644 index 1d9af90..0000000 --- a/src/Storage/Entity/Mapping.php +++ /dev/null @@ -1,25 +0,0 @@ - Date: Fri, 18 Apr 2025 22:31:26 +0200 Subject: [PATCH 3/6] Fixed test and implemented a basic test --- .../Axpecto/ClassBuilder/ClassBuilderTest.php | 4 +- .../Handler/RepositoryBuildHandlerTest.php | 96 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php diff --git a/tests/Axpecto/ClassBuilder/ClassBuilderTest.php b/tests/Axpecto/ClassBuilder/ClassBuilderTest.php index cd91a19..686fa0d 100644 --- a/tests/Axpecto/ClassBuilder/ClassBuilderTest.php +++ b/tests/Axpecto/ClassBuilder/ClassBuilderTest.php @@ -97,7 +97,7 @@ public function testBuildGeneratesProxyClass(): void { $result = $this->classBuilder->build( $class ); // Assert that a proxy class is generated and returned - $this->assertEquals( 'Axpecto_ClassBuilder_Tests_ClassBuilderTest', $result ); + $this->assertEquals( 'Axpecto_ClassBuilder_Tests_ClassBuilderTestProxy', $result ); } public function testGenerateProxyClass(): void { @@ -123,7 +123,7 @@ public function testGenerateProxyClass(): void { $proxiedClassName = $method->invoke( $this->classBuilder, $class, $buildOutput ); // Assert that the generated class name is correct - $this->assertEquals( 'Axpecto_ClassBuilder_Tests_SampleClass', $proxiedClassName ); + $this->assertEquals( 'Axpecto_ClassBuilder_Tests_SampleClassProxy', $proxiedClassName ); } } diff --git a/tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php b/tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php new file mode 100644 index 0000000..44e964f --- /dev/null +++ b/tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php @@ -0,0 +1,96 @@ +createMock( ReflectionUtils::class ); + $parser = $this->createMock( RepositoryMethodNameParser::class ); + $metadata = $this->createMock( EntityMetadataService::class ); + + $handler = new RepositoryBuildHandler( $reflect, $parser, $metadata ); + + $this->expectException( \InvalidArgumentException::class ); + + $annotation = $this->createMock( Annotation::class ); + $context = $this->createMock( BuildContext::class ); + + $handler->intercept( $annotation, $context ); + } + + public function testInterceptGeneratesMethod(): void { + $reflect = $this->createMock( ReflectionUtils::class ); + $parser = $this->createMock( RepositoryMethodNameParser::class ); + $metadata = $this->createMock( EntityMetadataService::class ); + + $handler = new RepositoryBuildHandler( $reflect, $parser, $metadata ); + + // Mocks + $repositoryAnnotation = new Repository( entityClass: DummyEntity::class ); + $repositoryAnnotation->setAnnotatedClass( DummyRepository::class ); + + $entityAnnotation = new Entity( storage: DummyStorage::class, table: 'dummy' ); + + $reflect->method( 'getClassAnnotations' ) + ->willReturn( listOf( $entityAnnotation ) ); + + $method = new ReflectionMethod( DummyRepository::class, 'findByIdAndName' ); + + $reflect->method( 'getAbstractMethods' ) + ->willReturn( listOf( $method ) ); + + $reflect->method( 'getMethodDefinitionString' ) + ->willReturn( 'public function findByIdAndName($id, $name)' ); + + $parser->method( 'parse' )->willReturn( + listOf( + new ParsedMethodPart( Prefix::GET_BY, LogicOperator::AND, 'id', Operator::EQUALS ), + new ParsedMethodPart( Prefix::GET_BY, LogicOperator::AND, 'name', Operator::EQUALS ) + ) + ); + + $metadata->method( 'getFields' )->willReturn( + listOf( + new EntityField( 'id', 'string', false, DummyEntity::class, persistenceMapping: 'id' ), + new EntityField( 'name', 'string', false, DummyEntity::class, persistenceMapping: 'name' ), + ) + ); + + $context = new BuildContext( DummyRepository::class ); + + $handler->intercept( $repositoryAnnotation, $context ); + + $this->assertTrue( $context->methods->offsetExists( 'findByIdAndName' ) ); + $this->assertStringContainsString( 'addCondition', $context->methods['findByIdAndName'] ); + } +} + +// Dummy classes for the test + +interface DummyRepository { + public function findByIdAndName( $id, $name ); +} + +class DummyEntity { + public function __construct( + public int $id, + public string $name + ) { + } +} + +class DummyStorage { +} From 6bc0fb4a6af08032784a21e09ece2a56af3bf9a4 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 18 Apr 2025 22:33:46 +0200 Subject: [PATCH 4/6] Improved the test so that we also assert that the field mapping is working as expected --- .../Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php b/tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php index 44e964f..066d439 100644 --- a/tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php +++ b/tests/Axpecto/Repository/Handler/RepositoryBuildHandlerTest.php @@ -65,7 +65,7 @@ public function testInterceptGeneratesMethod(): void { $metadata->method( 'getFields' )->willReturn( listOf( new EntityField( 'id', 'string', false, DummyEntity::class, persistenceMapping: 'id' ), - new EntityField( 'name', 'string', false, DummyEntity::class, persistenceMapping: 'name' ), + new EntityField( 'name', 'string', false, DummyEntity::class, persistenceMapping: 'name_mapping' ), ) ); @@ -75,6 +75,7 @@ public function testInterceptGeneratesMethod(): void { $this->assertTrue( $context->methods->offsetExists( 'findByIdAndName' ) ); $this->assertStringContainsString( 'addCondition', $context->methods['findByIdAndName'] ); + $this->assertStringContainsString( 'name_mapping', $context->methods['findByIdAndName'] ); } } From e440a8fd9ba957f75d92135a6a03481e988f744e Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 18 Apr 2025 22:35:25 +0200 Subject: [PATCH 5/6] Fixed Psalm error --- src/ClassBuilder/BuildContext.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ClassBuilder/BuildContext.php b/src/ClassBuilder/BuildContext.php index aced5d7..b6d8644 100644 --- a/src/ClassBuilder/BuildContext.php +++ b/src/ClassBuilder/BuildContext.php @@ -25,7 +25,9 @@ class BuildContext { /** * Constructor for the BuildOutput class. * - * @param Kmap $methods List of methods in the output. + * @psalm-suppress PossiblyUnusedProperty + * + * @param Kmap $methods List of methods in the output. * @param Kmap $properties List of class properties in the output. */ public function __construct( @@ -40,8 +42,8 @@ public function __construct( * Add a method with its signature and implementation to the output. * Modifies the internal state directly. * - * @param string $name The method name. - * @param string $signature The method signature. + * @param string $name The method name. + * @param string $signature The method signature. * @param string $implementation The method implementation. * * @return void @@ -55,7 +57,7 @@ public function addMethod( string $name, string $signature, string $implementati * Add a property to the output. * Modifies the internal state directly. * - * @param string $name The property name. + * @param string $name The property name. * @param string $implementation The property implementation. * * @return void @@ -76,7 +78,7 @@ public function addProperty( string $name, string $implementation ): void { */ public function injectProperty( string $name, string $class ): string { $this->addProperty( - name: $class, + name: $class, implementation: "#[" . Inject::class . "] private $class \$$name;", ); @@ -89,10 +91,10 @@ public function injectProperty( string $name, string $class ): string { * * @psalm-suppress PossiblyUnusedMethod * - * @param Kmap $methods List of methods to append. + * @param Kmap $methods List of methods to append. * @param Kmap $properties List of properties to append. * -// * @return void + * // * @return void */ public function add( Kmap $methods, Kmap $properties ): void { $this->methods->merge( $methods ); From ad5a9f2352aa33c97b1940081f60a066c4d4eb4b Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 18 Apr 2025 22:36:23 +0200 Subject: [PATCH 6/6] Fixed Psalm error 2nd try --- src/ClassBuilder/BuildContext.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ClassBuilder/BuildContext.php b/src/ClassBuilder/BuildContext.php index b6d8644..ee806cb 100644 --- a/src/ClassBuilder/BuildContext.php +++ b/src/ClassBuilder/BuildContext.php @@ -7,6 +7,7 @@ use Exception; /** + * @psalm-suppress PossiblyUnusedProperty * Class BuildOutput * * This class encapsulates the output of a build process for handling methods and properties @@ -24,9 +25,7 @@ class BuildContext { /** * Constructor for the BuildOutput class. - * - * @psalm-suppress PossiblyUnusedProperty - * + ** * @param Kmap $methods List of methods in the output. * @param Kmap $properties List of class properties in the output. */