From bedb90238bdc2f1bfeebc69b6545a0cfc1d42c3c Mon Sep 17 00:00:00 2001 From: Paul Mansell Date: Mon, 28 Jun 2021 12:00:13 +0100 Subject: [PATCH] Implemented Join Types --- Documentation/Parsers/Doctrine.md | 10 +- src/Parser/Doctrine/DoctrineParser.php | 140 ++++++++++++------ src/Parser/Doctrine/JoinPartialParser.php | 7 +- tests/Model/RuleGroupTest.php | 2 +- .../Parser/Doctrine/JoinPartialParserTest.php | 3 +- ...ockEntityWithAssociationDoctrineParser.php | 16 +- ...MockEntityWithEmbeddableDoctrineParser.php | 3 + 7 files changed, 125 insertions(+), 56 deletions(-) diff --git a/Documentation/Parsers/Doctrine.md b/Documentation/Parsers/Doctrine.md index c776cf9..402b2ee 100644 --- a/Documentation/Parsers/Doctrine.md +++ b/Documentation/Parsers/Doctrine.md @@ -7,7 +7,8 @@ #### Usage - When you are parsing a `$jsonString` for a particular Doctrine Entity, create an instance of `DoctrineParser`. -- Don't forget to construct this instance with the `$classname` of the Doctrine Entity, the `$queryBuilderFieldsToEntityProperties`, and `$queryBuilderFieldPrefixesToAssociationClasses`. +- Don't forget to construct this instance with the `$classname` of the Doctrine Entity, the + `$queryBuilderFieldsToEntityProperties`, `$queryBuilderFieldPrefixesToAssociationClasses` and `$fieldPrefixesJoinType`. ## Example @@ -110,6 +111,9 @@ use YourNamespace\YourApp\Entity\Label; ], [ 'labels'=>Label::class, + ], + [ + 'labels' => 'left', ] ); @@ -144,6 +148,10 @@ Now supporting Doctrine embeddables. For example: [ 'associationEntity' => AssociationEntity::class, ], + // $fieldPrefixesJoinType + [ + 'associationEntity' => 'left', + ], // $embeddableFieldsToProperties [ 'embeddable.startDate' => 'embeddable.startDate', diff --git a/src/Parser/Doctrine/DoctrineParser.php b/src/Parser/Doctrine/DoctrineParser.php index 5165af9..e684648 100644 --- a/src/Parser/Doctrine/DoctrineParser.php +++ b/src/Parser/Doctrine/DoctrineParser.php @@ -49,14 +49,21 @@ class DoctrineParser implements DoctrineParserInterface */ private $embeddableFieldPrefixesToEmbeddableClasses; + /** + * @var array + */ + private $fieldPrefixesJoinType; + + /** * @param string $className - * @param array $fieldsToProperties - * @param array $fieldPrefixesToClasses - * @param array $embeddableFieldsToProperties - * @param array $embeddableInsideEmbeddableFieldsToProperties - * @param array $embeddableFieldPrefixesToClasses - * @param array $embeddableFieldPrefixesToEmbeddableClasses + * @param array $fieldsToProperties + * @param array $fieldPrefixesToClasses + * @param array $embeddableFieldsToProperties + * @param array $embeddableInsideEmbeddableFieldsToProperties + * @param array $embeddableFieldPrefixesToClasses + * @param array $embeddableFieldPrefixesToEmbeddableClasses + * @param array $fieldPrefixesJoinType * * @see MockEntityWithEmbeddableDoctrineParser for full example */ @@ -64,14 +71,17 @@ public function __construct( string $className, array $fieldsToProperties, array $fieldPrefixesToClasses = [], + array $fieldPrefixesJoinType = [], array $embeddableFieldsToProperties = [], array $embeddableInsideEmbeddableFieldsToProperties = [], array $embeddableFieldPrefixesToClasses = [], array $embeddableFieldPrefixesToEmbeddableClasses = [] + ) { $this->className = $className; $this->fieldsToProperties = $fieldsToProperties; $this->fieldPrefixesToClasses = $fieldPrefixesToClasses; + $this->fieldPrefixesJoinType = $fieldPrefixesJoinType; $this->embeddableFieldsToProperties = $embeddableFieldsToProperties; $this->embeddableInsideEmbeddableFieldsToProperties = $embeddableInsideEmbeddableFieldsToProperties; $this->embeddableFieldPrefixesToClasses = $embeddableFieldPrefixesToClasses; @@ -88,7 +98,7 @@ final public function parse(RuleGroupInterface $ruleGroup, array $sortColumns = { $selectString = SelectPartialParser::parse($this->fieldPrefixesToClasses); $fromString = FromPartialParser::parse($this->className); - $joinString = JoinPartialParser::parse($this->fieldPrefixesToClasses); + $joinString = JoinPartialParser::parse($this->fieldPrefixesToClasses, $this->fieldPrefixesJoinType); $whereParsedRuleGroup = WherePartialParser::parse( $this->fieldsToProperties, @@ -107,7 +117,11 @@ final public function parse(RuleGroupInterface $ruleGroup, array $sortColumns = $this->embeddableInsideEmbeddableFieldsToProperties ); - $dqlString = preg_replace('/\s+/', ' ', $selectString.$fromString.$joinString.$whereString.$orderString); + $dqlString = preg_replace( + '/\s+/', + ' ', + $selectString . $fromString . $joinString . $whereString . $orderString + ); return new ParsedRuleGroup($dqlString, $parameters, $this->className); // preg_replace -> no more than one space } @@ -120,11 +134,21 @@ final private function validate() $this->validateClass($this->className); $this->validateFieldsToProperties($this->fieldsToProperties, $this->fieldPrefixesToClasses); $this->validateFieldPrefixesToClasses($this->fieldPrefixesToClasses); - $allEmbeddableFields = array_merge($this->embeddableFieldsToProperties, $this->embeddableInsideEmbeddableFieldsToProperties); - $allEmbeddablePrefixesToClasses = array_merge($this->embeddableFieldPrefixesToClasses, $this->embeddableFieldPrefixesToEmbeddableClasses); + $allEmbeddableFields = array_merge( + $this->embeddableFieldsToProperties, + $this->embeddableInsideEmbeddableFieldsToProperties + ); + $allEmbeddablePrefixesToClasses = array_merge( + $this->embeddableFieldPrefixesToClasses, + $this->embeddableFieldPrefixesToEmbeddableClasses + ); $this->validateFieldsToProperties($allEmbeddableFields, $allEmbeddablePrefixesToClasses); $this->validateFieldPrefixesToClasses($allEmbeddablePrefixesToClasses); - $this->validateEmbeddableFieldPrefixes($this->embeddableFieldPrefixesToClasses, $this->embeddableFieldPrefixesToEmbeddableClasses); + $this->validateEmbeddableFieldPrefixes( + $this->embeddableFieldPrefixesToClasses, + $this->embeddableFieldPrefixesToEmbeddableClasses + ); + $this->validateJoinTypes($this->fieldPrefixesToClasses, $this->fieldPrefixesJoinType); } /** @@ -169,12 +193,14 @@ final private function validateFieldPrefixesToClasses(array $fieldPrefixesToClas $fieldPrefixPrefix = preg_replace($suffixPattern, '', $fieldPrefix); $fieldSuffix = str_replace('.', '', $suffixMatches[0]); // remove preceding dot if (!array_key_exists($fieldPrefixPrefix, $fieldPrefixesToClasses)) { - throw new MissingAssociationClassException(sprintf( - 'Missing association class for queryBuilderFieldPrefix %s, at class %s, for parser %s', - $fieldPrefixPrefix, - $this->className, - static::class - )); + throw new MissingAssociationClassException( + sprintf( + 'Missing association class for queryBuilderFieldPrefix %s, at class %s, for parser %s', + $fieldPrefixPrefix, + $this->className, + static::class + ) + ); } $classForThisPrefix = $fieldPrefixesToClasses[$fieldPrefixPrefix]; $this->validateClassHasProperty($classForThisPrefix, $fieldSuffix); @@ -190,18 +216,22 @@ final private function validateFieldPrefixesToClasses(array $fieldPrefixesToClas * @param array $embeddableFieldPrefixesToClasses * @param array $embeddableFieldPrefixesToEmbeddableClasses */ - final private function validateEmbeddableFieldPrefixes(array $embeddableFieldPrefixesToClasses, array $embeddableFieldPrefixesToEmbeddableClasses) - { + final private function validateEmbeddableFieldPrefixes( + array $embeddableFieldPrefixesToClasses, + array $embeddableFieldPrefixesToEmbeddableClasses + ) { $prefixes = array_keys($embeddableFieldPrefixesToClasses); foreach ($prefixes as $prefix) { if (array_key_exists($prefix, $embeddableFieldPrefixesToEmbeddableClasses)) { - throw new DuplicatePrefixException(sprintf( - 'Duplicate embeddable field prefix %s, at class %s, for parser %s', - $prefix, - $this->className, - static::class - )); + throw new DuplicatePrefixException( + sprintf( + 'Duplicate embeddable field prefix %s, at class %s, for parser %s', + $prefix, + $this->className, + static::class + ) + ); } } } @@ -209,18 +239,20 @@ final private function validateEmbeddableFieldPrefixes(array $embeddableFieldPre /** * @param string $className * + * @throws InvalidClassNameException * @see http://symfony.com/doc/current/components/property_info.html#components-property-info-extractors * - * @throws InvalidClassNameException */ final private function validateClass(string $className) { if (!class_exists($className)) { - throw new InvalidClassNameException(sprintf( - 'Expected valid class name in %s. %s was given, and it is not a valid class name.', - static::class, - $className - )); + throw new InvalidClassNameException( + sprintf( + 'Expected valid class name in %s. %s was given, and it is not a valid class name.', + static::class, + $className + ) + ); } } @@ -228,9 +260,9 @@ final private function validateClass(string $className) * @param string $className * @param string $classProperty * + * @throws FieldMappingException * @see http://symfony.com/doc/current/components/property_info.html#components-property-info-extractors * - * @throws FieldMappingException */ final private function validateClassHasProperty(string $className, string $classProperty) { @@ -238,29 +270,49 @@ final private function validateClassHasProperty(string $className, string $class $properties = $propertyInfo->getProperties($className); if (!in_array($classProperty, $properties)) { - throw new FieldMappingException(sprintf( - 'Property %s is not accessible in %s.', - $classProperty, - $className - )); + throw new FieldMappingException( + sprintf( + 'Property %s is not accessible in %s.', + $classProperty, + $className + ) + ); } } /** * @param string $fieldPrefix - * @param array $fieldPrefixesToClasses + * @param array $fieldPrefixesToClasses * * @throws MissingAssociationClassException */ final private function validateFieldPrefixIsInAssociations(string $fieldPrefix, array $fieldPrefixesToClasses) { if (!array_key_exists($fieldPrefix, $fieldPrefixesToClasses)) { - throw new MissingAssociationClassException(sprintf( - 'Missing class for fieldPrefix %s, at class %s, for parser %s', - $fieldPrefix, - $this->className, - static::class - )); + throw new MissingAssociationClassException( + sprintf( + 'Missing class for fieldPrefix %s, at class %s, for parser %s', + $fieldPrefix, + $this->className, + static::class + ) + ); + } + } + + final private function validateJoinTypes(array $fieldPrefixesToClasses, array $fieldPrefixesJoinType) + { + foreach ($fieldPrefixesToClasses as $prefix => $class) { + if (!array_key_exists($prefix, $fieldPrefixesJoinType)) { + throw new MissingAssociationClassException( + sprintf( + 'Missing Join Type for fieldPrefix %s, at class %s, for parser %s', + $prefix, + $this->className, + static::class + ) + ); + } } } } diff --git a/src/Parser/Doctrine/JoinPartialParser.php b/src/Parser/Doctrine/JoinPartialParser.php index 5a18663..2c9933e 100644 --- a/src/Parser/Doctrine/JoinPartialParser.php +++ b/src/Parser/Doctrine/JoinPartialParser.php @@ -13,8 +13,8 @@ final private function __construct() * * @return string */ - final public static function parse(array $queryBuilderFieldPrefixesToAssociationClasses): string - { + final public static function parse(array $queryBuilderFieldPrefixesToAssociationClasses, array $prefixJoinType): + string { $joinString = ''; foreach ($queryBuilderFieldPrefixesToAssociationClasses as $queryBuilderPrefix => $associationClass) { $joinPart = sprintf( @@ -23,7 +23,8 @@ final public static function parse(array $queryBuilderFieldPrefixesToAssociation $queryBuilderPrefix ); $joinString .= sprintf( - ' LEFT JOIN %s %s ', + ' %s JOIN %s %s ', + strtoupper($prefixJoinType[$queryBuilderPrefix]), StringManipulator::replaceAllDotsExceptLast($joinPart), StringManipulator::replaceAllDots($joinPart) ); diff --git a/tests/Model/RuleGroupTest.php b/tests/Model/RuleGroupTest.php index 132f730..967aec0 100644 --- a/tests/Model/RuleGroupTest.php +++ b/tests/Model/RuleGroupTest.php @@ -14,7 +14,7 @@ class RuleGroupTest extends TestCase */ private $ruleGroup; - public function setup() + public function setup(): void { // do both! neither of these should render an exception diff --git a/tests/Parser/Doctrine/JoinPartialParserTest.php b/tests/Parser/Doctrine/JoinPartialParserTest.php index 16718ca..a1c8592 100644 --- a/tests/Parser/Doctrine/JoinPartialParserTest.php +++ b/tests/Parser/Doctrine/JoinPartialParserTest.php @@ -17,7 +17,8 @@ public function parseTest() 'specification' => 'Valid_Class_Is_Not_Checked', 'labels.specification' => 'Valid_Class_Is_Not_Checked', ]; - $parsed = JoinPartialParser::parse($queryBuilderFieldPrefixesToAssociationClasses); + $joinPrefixes = ['labels' => 'left', 'specification' => 'left', 'labels.specification' => 'left']; + $parsed = JoinPartialParser::parse($queryBuilderFieldPrefixesToAssociationClasses, $joinPrefixes); $expected = ' LEFT JOIN object.labels object_labels LEFT JOIN object.specification object_specification LEFT JOIN object_labels.specification object_labels_specification '; self::assertEquals($expected, $parsed); diff --git a/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithAssociationDoctrineParser.php b/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithAssociationDoctrineParser.php index d6006a1..94d8835 100644 --- a/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithAssociationDoctrineParser.php +++ b/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithAssociationDoctrineParser.php @@ -10,16 +10,20 @@ class MockEntityWithAssociationDoctrineParser extends DoctrineParser { public function __construct() { - parent::__construct(MockEntity::class, + parent::__construct( + MockEntity::class, [ - 'id' => 'id', - 'price' => 'price', - 'name' => 'name', - 'date' => 'date', - 'associationEntity.id' => 'associationEntity.id', + 'id' => 'id', + 'price' => 'price', + 'name' => 'name', + 'date' => 'date', + 'associationEntity.id' => 'associationEntity.id', ], [ 'associationEntity' => MockEntityAssociation::class, + ], + [ + 'associationEntity' => 'left', ] ); } diff --git a/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithEmbeddableDoctrineParser.php b/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithEmbeddableDoctrineParser.php index ac2a75f..2d8230a 100644 --- a/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithEmbeddableDoctrineParser.php +++ b/tests/Util/Doctrine/Mock/DoctrineParser/MockEntityWithEmbeddableDoctrineParser.php @@ -24,6 +24,9 @@ public function __construct() [ 'associationEntity' => MockEntityAssociation::class, ], + [ + 'associationEntity' => 'left', + ], [ 'embeddable.startDate' => 'embeddable.startDate', 'embeddable.endDate' => 'embeddable.endDate',