Skip to content
Open
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
10 changes: 9 additions & 1 deletion Documentation/Parsers/Doctrine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -110,6 +111,9 @@ use YourNamespace\YourApp\Entity\Label;
],
[
'labels'=>Label::class,
],
[
'labels' => 'left',
]
);

Expand Down Expand Up @@ -144,6 +148,10 @@ Now supporting Doctrine embeddables. For example:
[
'associationEntity' => AssociationEntity::class,
],
// $fieldPrefixesJoinType
[
'associationEntity' => 'left',
],
// $embeddableFieldsToProperties
[
'embeddable.startDate' => 'embeddable.startDate',
Expand Down
140 changes: 96 additions & 44 deletions src/Parser/Doctrine/DoctrineParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,39 @@ 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
*/
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;
Expand All @@ -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,
Expand All @@ -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
}
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -190,77 +216,103 @@ 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
)
);
}
}
}

/**
* @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
)
);
}
}

/**
* @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)
{
$propertyInfo = new PropertyInfoExtractor([new ReflectionExtractor()]);
$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
)
);
}
}
}
}
7 changes: 4 additions & 3 deletions src/Parser/Doctrine/JoinPartialParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
);
Expand Down
2 changes: 1 addition & 1 deletion tests/Model/RuleGroupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion tests/Parser/Doctrine/JoinPartialParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public function __construct()
[
'associationEntity' => MockEntityAssociation::class,
],
[
'associationEntity' => 'left',
],
[
'embeddable.startDate' => 'embeddable.startDate',
'embeddable.endDate' => 'embeddable.endDate',
Expand Down