diff --git a/Command/ValidateDataSchemaCommand.php b/Command/ValidateDataSchemaCommand.php
new file mode 100644
index 0000000..2a35d1e
--- /dev/null
+++ b/Command/ValidateDataSchemaCommand.php
@@ -0,0 +1,105 @@
+setName('glavweb:data-schema:validate')
+ ->setDescription('Validates data schema configuration file')
+ ->addArgument(
+ 'path',
+ InputArgument::OPTIONAL,
+ 'File path relative to directory defined in "data_schema.dir" bundle configuration parameter. Validates all files in folder if not specified.'
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ /** @var DataSchemaValidator $dataSchemaValidator */
+ $dataSchemaValidator = $this->getContainer()->get('glavweb_data_schema.validator');
+ $rootDir = $this->getContainer()->getParameter('glavweb_data_schema.data_schema_dir');
+ $nestingDepth = $this->getContainer()->getParameter('glavweb_data_schema.data_schema_max_nesting_depth');
+
+ $successful = false;
+ $path = $input->getArgument('path');
+
+ $output->writeln(
+ [
+ 'DataSchema Validator',
+ '====================',
+ '',
+ ]
+ );
+
+ if ($path) {
+ try {
+ $dataSchemaValidator->validateFile($path, $nestingDepth);
+
+ $successful = true;
+ $output->writeln('Validation successful');
+
+ } catch (\Exception $e) {
+ $output->writeln(sprintf('%s', $e->getMessage()));
+ $output->writeln('Validation failed');
+ }
+
+ } else {
+ $finder = new Finder();
+ $finder->in($rootDir)->files();
+ $totalCount = $finder->count();
+ $errorsCount = 0;
+
+ if (!$totalCount) {
+ $output->writeln(sprintf('Files not found in "%s" directory', $rootDir));
+
+ return 0;
+ }
+
+ $output->writeln([sprintf('Validating %s configuration files...', $totalCount), '']);
+
+ foreach ($finder as $file) {
+ try {
+ $dataSchemaValidator->validateFile($file->getRelativePathname(), $nestingDepth);
+ } catch (\Exception $e) {
+ $errorsCount++;
+
+ $output->writeln(
+ [
+ sprintf('%s:', $file->getRelativePathname()),
+ '--------------------',
+ sprintf('%s', $e->getMessage()),
+ ''
+ ]
+ );
+ }
+ }
+
+ if ($errorsCount) {
+ $output->writeln(
+ sprintf('Validation failed. %s configuration files have errors.', $errorsCount)
+ );
+
+ } else {
+ $successful = true;
+ $output->writeln('Validation successful');
+ }
+ }
+
+ return $successful ? 0 : 1;
+ }
+}
\ No newline at end of file
diff --git a/Configuration/DataSchemaConfiguration.php b/Configuration/DataSchemaConfiguration.php
new file mode 100755
index 0000000..b769005
--- /dev/null
+++ b/Configuration/DataSchemaConfiguration.php
@@ -0,0 +1,125 @@
+nestingDepth = $nestingDepth;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfigTreeBuilder()
+ {
+ $treeBuilder = new TreeBuilder();
+
+ $rootNode = $treeBuilder->root('schema');
+
+ $rootNode
+ ->children()
+ ->scalarNode('schema')
+ ->end()
+ ->enumNode('db_driver')
+ ->isRequired()
+ ->values(['orm'])
+ ->end()
+ ->scalarNode('class')
+ ->isRequired()
+ ->end()
+ ->arrayNode('roles')
+ ->beforeNormalization()
+ ->castToArray()
+ ->end()
+ ->end()
+ ->booleanNode('filter_null_values')
+ ->defaultTrue()
+ ->end()
+ ->arrayNode('query')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->arrayNode('selects')
+ ->useAttributeAsKey('name')
+ ->scalarPrototype()->end()
+ ->end()
+ ->end()
+ ->end()
+ ->append($this->addPropertiesNode($this->nestingDepth))
+ ->end()
+ ;
+
+
+ return $treeBuilder;
+ }
+
+ public function addPropertiesNode($depth)
+ {
+ $treeBuilder = new TreeBuilder();
+
+ $rootNode = $treeBuilder->root('properties');
+
+ if ($depth === 0) {
+ return $rootNode;
+ }
+
+ $rootNode
+ ->useAttributeAsKey('name')
+ ->arrayPrototype()
+ ->children()
+ ->scalarNode('schema')
+ ->defaultNull()
+ ->end()
+ ->scalarNode('class')
+ ->defaultNull()
+ ->end()
+ ->scalarNode('description')
+ ->defaultNull()
+ ->end()
+ ->scalarNode('discriminator')
+ ->defaultNull()
+ ->end()
+ ->booleanNode('filter_null_values')
+ ->defaultTrue()
+ ->end()
+ ->enumNode('join')
+ ->defaultValue('none')
+ ->values(['none', 'left', 'inner'])
+ ->end()
+ ->scalarNode('type')
+ ->defaultNull()
+ ->end()
+ ->scalarNode('source')
+ ->defaultNull()
+ ->end()
+ ->scalarNode('decode')
+ ->defaultNull()
+ ->end()
+ ->scalarNode('hidden')
+ ->defaultFalse()
+ ->end()
+ ->arrayNode('conditions')
+ ->scalarPrototype()->end()
+ ->end()
+ ->end()
+ ->append($this->addPropertiesNode(--$depth))
+ ->end()
+ ;
+
+ return $rootNode;
+ }
+}
\ No newline at end of file
diff --git a/DataSchema/DataSchema.php b/DataSchema/DataSchema.php
old mode 100644
new mode 100755
index b19ce31..a3743da
--- a/DataSchema/DataSchema.php
+++ b/DataSchema/DataSchema.php
@@ -11,50 +11,54 @@
namespace Glavweb\DataSchemaBundle\DataSchema;
-use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\ORM\Mapping\ClassMetadata;
+use Doctrine\ORM\Mapping\MappingException;
use Glavweb\DataSchemaBundle\DataSchema\Persister\PersisterFactory;
use Glavweb\DataSchemaBundle\DataSchema\Persister\PersisterInterface;
-use Glavweb\DataSchemaBundle\DataTransformer\DataTransformerRegistry;
use Glavweb\DataSchemaBundle\DataTransformer\TransformEvent;
use Glavweb\DataSchemaBundle\Exception\DataSchema\InvalidConfigurationException;
+use Glavweb\DataSchemaBundle\Exception\DataTransformer\DataTransformerNotExists;
use Glavweb\DataSchemaBundle\Hydrator\Doctrine\ObjectHydrator;
-use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Glavweb\DataSchemaBundle\Service\DataSchemaFilter;
+use Glavweb\DataSchemaBundle\Service\DataSchemaService;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Class DataSchema
*
- * @author Andrey Nilov
+ * @author Andrey Nilov
* @package Glavweb\DataSchemaBundle
*/
class DataSchema
{
+ public const DEFAULTS = [
+ 'schema' => null,
+ 'class' => null,
+ 'description' => null,
+ 'discriminator' => null,
+ 'filter_null_values' => true,
+ 'join' => 'none',
+ 'type' => null,
+ 'source' => null,
+ 'decode' => null,
+ 'hidden' => false,
+ 'conditions' => [],
+ 'roles' => [],
+ 'discriminatorColumnName' => null,
+ 'discriminatorMap' => [],
+ 'tableName' => null
+ ];
+
/**
* @var DataSchemaFactory
*/
private $dataSchemaFactory;
- /**
- * @var Registry
- */
- private $doctrine;
-
- /**
- * @var DataTransformerRegistry
- */
- private $dataTransformerRegistry;
-
/**
* @var PersisterInterface
*/
private $persister;
- /**
- * @var AuthorizationCheckerInterface
- */
- private $authorizationChecker;
-
/**
* @var Placeholder
*/
@@ -63,22 +67,17 @@ class DataSchema
/**
* @var array
*/
- private $configuration = [];
+ private $configuration;
/**
* @var array
*/
- private $scopeConfig = [];
-
- /**
- * @var ClassMetadata[]
- */
- private $classMetadataCache;
+ private $scopeConfig;
/**
- * @var bool
+ * @var int
*/
- private $withoutAssociations;
+ private $nestingDepth;
/**
* @var string|null
@@ -90,212 +89,408 @@ class DataSchema
*/
private $objectHydrator;
+ /**
+ * @var DataSchemaService
+ */
+ private $dataSchemaService;
+
+ /**
+ * @var DataSchemaFilter
+ */
+ private $dataSchemaFilter;
+
/**
* DataSchema constructor.
*
- * @param DataSchemaFactory $dataSchemaFactory
- * @param Registry $doctrine
- * @param DataTransformerRegistry $dataTransformerRegistry
- * @param PersisterFactory $persisterFactory
- * @param AuthorizationCheckerInterface $authorizationChecker
- * @param Placeholder $placeholder
- * @param ObjectHydrator $objectHydrator
- * @param array $configuration
- * @param array $scopeConfig
- * @param bool $withoutAssociations
- * @param string|null $defaultHydratorMode
+ * @param DataSchemaFactory $dataSchemaFactory
+ * @param DataSchemaService $dataSchemaService
+ * @param DataSchemaFilter $dataSchemaFilter
+ * @param PersisterFactory $persisterFactory
+ * @param Placeholder $placeholder
+ * @param ObjectHydrator $objectHydrator
+ * @param array $configuration
+ * @param array|null $scopeConfig
+ * @param int|null $nestingDepth
+ * @param string|null $defaultHydratorMode
+ * @throws InvalidConfigurationException
+ * @throws MappingException
*/
- public function __construct(
- DataSchemaFactory $dataSchemaFactory,
- Registry $doctrine,
- DataTransformerRegistry $dataTransformerRegistry,
- PersisterFactory $persisterFactory,
- AuthorizationCheckerInterface $authorizationChecker,
- Placeholder $placeholder,
- ObjectHydrator $objectHydrator,
- array $configuration,
- array $scopeConfig = null,
- bool $withoutAssociations = false,
- $defaultHydratorMode = null
- ) {
- $this->dataSchemaFactory = $dataSchemaFactory;
- $this->doctrine = $doctrine;
- $this->dataTransformerRegistry = $dataTransformerRegistry;
- $this->authorizationChecker = $authorizationChecker;
- $this->placeholder = $placeholder;
- $this->objectHydrator = $objectHydrator;
- $this->withoutAssociations = $withoutAssociations;
- $this->defaultHydratorMode = $defaultHydratorMode;
-
- if (!isset($configuration['class'])) {
- $configuration['class'] = null;
- }
- $class = $configuration['class'];
+ public function __construct(DataSchemaFactory $dataSchemaFactory,
+ DataSchemaService $dataSchemaService,
+ DataSchemaFilter $dataSchemaFilter,
+ PersisterFactory $persisterFactory,
+ Placeholder $placeholder,
+ ObjectHydrator $objectHydrator,
+ array $configuration,
+ array $scopeConfig = null,
+ int $nestingDepth = null,
+ string $defaultHydratorMode = null)
+ {
+ $this->dataSchemaFactory = $dataSchemaFactory;
+ $this->dataSchemaService = $dataSchemaService;
+ $this->dataSchemaFilter = $dataSchemaFilter;
+ $this->placeholder = $placeholder;
+ $this->objectHydrator = $objectHydrator;
+ $this->nestingDepth = $nestingDepth;
+ $this->defaultHydratorMode = $defaultHydratorMode;
+
+ $this->persister = $persisterFactory->createPersister($configuration['db_driver'], $this);
+ $this->scopeConfig = $scopeConfig;
- if (!isset($configuration['db_driver'])) {
- throw new \RuntimeException('Option "db_driver" must be defined.');
- }
+ $this->dataSchemaService->startStopwatch('filter');
+
+ $configuration = $this->dataSchemaFilter->filter($configuration, $scopeConfig, $nestingDepth);
+
+ $this->dataSchemaService->stopStopwatch('filter');
+ $this->dataSchemaService->startStopwatch('prepareConfiguration');
+
+ $this->configuration =
+ $this->prepareConfiguration($configuration, $configuration['class'], $scopeConfig, $this->nestingDepth);
+ $this->dataSchemaService->stopStopwatch('prepareConfiguration');
- $this->persister = $persisterFactory->createPersister($configuration['db_driver'], $this);
- $this->scopeConfig = $scopeConfig;
- $this->configuration = $this->prepareConfiguration($configuration, $class, $scopeConfig);
}
/**
- * @return array
+ * @param string $propertyName
+ * @return bool
*/
- public function getConfiguration()
+ public function hasProperty(string $propertyName): bool
{
- return $this->configuration;
+ return $this->getPropertyConfiguration($propertyName) !== null;
+ }
+
+ /**
+ * @param string $propertyName
+ * @return bool
+ */
+ public function hasPropertyInDb(string $propertyName): bool
+ {
+ $propertyConfiguration = $this->getPropertyConfiguration($propertyName);
+
+ return $propertyConfiguration !== null && isset($propertyConfiguration['from_db'])
+ && $propertyConfiguration['from_db'];
+ }
+
+ /**
+ * @param string $condition
+ * @param string $alias
+ * @param UserInterface|null $user
+ * @return string
+ */
+ public function conditionPlaceholder(string $condition, string $alias, UserInterface $user = null): string
+ {
+ return $this->placeholder->condition($condition, $alias, $user);
}
/**
- * @param array $data
- * @param array $config
- * @param array $scopeConfig
- * @param string $parentClassName
- * @param string $parentPropertyName
- * @param array $defaultData
+ * @param array $configuration
+ * @param string|null $class
+ * @param array|null $scopeConfig
+ * @param int $nestingDepth
* @return array
- * @throws \Doctrine\ORM\Mapping\MappingException
+ * @throws InvalidConfigurationException
+ * @throws MappingException
*/
- public function getData(array $data, array $config = null, array $scopeConfig = null, $parentClassName = null, $parentPropertyName = null, $defaultData = [])
+ protected function prepareConfiguration(array $configuration,
+ ?string $class,
+ array $scopeConfig = null,
+ int $nestingDepth = 0): array
{
- if ($config === null) {
- $config = $this->configuration;
- }
+ $class = $class ?? $configuration['class'] ?? null;
- if ($scopeConfig === null) {
- $scopeConfig = $this->scopeConfig;
- }
+ $configuration = array_replace(self::DEFAULTS, $configuration);
+ $configuration['class'] = $class;
- if (!$data) {
- return $defaultData;
+ if (!$this->dataSchemaFilter->isGranted($configuration['roles'])) {
+ return [];
}
- if (!isset($config['properties'])) {
- return $defaultData;
- }
+ // class
+ $classMetadata = $class ? $this->dataSchemaService->getClassMetadata($class) : null;
+ $identifierFieldNames = [];
+ $discriminatorMap = null;
- $preparedData = [];
+ if ($classMetadata instanceof ClassMetadata) {
+ if ($classMetadata->subClasses) {
+ $configuration['discriminatorColumnName'] = $classMetadata->discriminatorColumn['name'];
+ $configuration['discriminatorMap'] = $classMetadata->discriminatorMap;
+ $discriminatorMap = $configuration['discriminatorMap'];
+ }
- $class = $config['class'];
- if ($config['discriminatorMap'] && isset($data[$config['discriminatorColumnName']])) {
- $discriminator = $data[$config['discriminatorColumnName']];
- $class = $config['discriminatorMap'][$discriminator];
+ $configuration['tableName'] = $classMetadata->getTableName();
+ $identifierFieldNames = $classMetadata->getIdentifierFieldNames();
}
+ $configProperties = $configuration['properties'] ?? [];
+ $properties = [];
- foreach ($config['properties'] as $propertyName => $propertyConfig) {
- if (isset($propertyConfig['hidden']) && $propertyConfig['hidden'] == true) {
- continue;
+ foreach ($identifierFieldNames as $idName) {
+ if (!array_key_exists($idName, $configProperties)) {
+ $configProperties[$idName] = array_merge(self::DEFAULTS, ['hidden' => true]);
+ $configuration['properties'][$idName] = $configProperties[$idName];
}
+ }
- $propertyScopeConfig = null;
- if ($scopeConfig !== null) {
- if (!array_key_exists($propertyName, $scopeConfig)) {
- continue;
- }
+ foreach ($configProperties as $propertyName => $propertyConfig) {
+ $propertyScopeConfig = $scopeConfig[$propertyName] ?? null;
+ $schema = $propertyConfig['schema'] ?? null;
+ $isNested = $schema || !empty($propertyConfig['properties']);
- $propertyScopeConfig = $scopeConfig[$propertyName] ?: [];
+ if ($schema) {
+ $propertyConfig = $this->getNestedDataSchemaConfiguration(
+ $schema,
+ $propertyConfig,
+ $nestingDepth - 1,
+ $propertyScopeConfig
+ );
}
- if (array_key_exists($propertyName, $data)) {
- $value = $data[$propertyName];
+ $discriminator = $propertyConfig['discriminator'] ?? null;
+ $subClass = $discriminatorMap && $discriminator ? $discriminatorMap[$discriminator] ?? null : null;
- if ($value === null) {
- if (in_array($propertyConfig['type'], ['array', 'json_array'])) {
- $value = [];
+ $propertyOwnerClassMetadata =
+ $subClass ? $this->dataSchemaService->getClassMetadata($subClass) : $classMetadata;
- } elseif ($config['filter_null_values']) {
- continue;
- }
+ // set default description
+ if (empty($propertyConfig['description']) && $propertyOwnerClassMetadata instanceof ClassMetadata
+ && $propertyOwnerClassMetadata->hasField($propertyName)) {
+
+ $fieldMapping = $propertyOwnerClassMetadata->getFieldMapping($propertyName);
+ $description = $fieldMapping['options']['comment'] ?? null;
+
+ $propertyConfig['description'] = $description;
+ }
+
+ if ($isNested) {
+ $propertyClass = $propertyConfig['class'] ?? null;
+
+ if (!$propertyClass && $propertyOwnerClassMetadata instanceof ClassMetadata
+ && $propertyOwnerClassMetadata->hasAssociation($propertyName)) {
+
+ $propertyClass = $propertyOwnerClassMetadata->getAssociationTargetClass($propertyName);
}
- } elseif (isset($propertyConfig['source']) && isset($data[$propertyConfig['source']])) {
- $value = $data[$propertyConfig['source']];
+ $propertyConfig = $this->prepareConfiguration(
+ $propertyConfig,
+ $propertyClass,
+ $propertyScopeConfig,
+ $nestingDepth - 1
+ );
- // if property is nested object
- } elseif (isset($propertyConfig['class']) && isset($propertyConfig['properties'])) {
- $metadata = $this->getClassMetadata($class);
- if (!$metadata->hasAssociation($propertyName)) {
- continue;
+ if ($propertyConfig && $propertyOwnerClassMetadata instanceof ClassMetadata
+ && $propertyOwnerClassMetadata->hasAssociation($propertyName)) {
+
+ $isCollection = $propertyOwnerClassMetadata->isCollectionValuedAssociation($propertyName);
+
+ $propertyConfig['type'] = $isCollection ? 'collection' : 'entity';
}
- $associationMapping = $metadata->getAssociationMapping($propertyName);
- $databaseFields = self::getDatabaseFields($propertyConfig, $propertyScopeConfig);
- $conditions = $propertyConfig['conditions'];
+ } else if ($propertyOwnerClassMetadata instanceof ClassMetadata) {
+ $propertyConfig['type'] =
+ $propertyConfig['type'] ?? $propertyOwnerClassMetadata->getTypeOfField($propertyName);
- switch ($associationMapping['type']) {
- case ClassMetadata::MANY_TO_MANY:
- $orderByExpressions = isset($associationMapping['orderBy']) ? $associationMapping['orderBy'] : [];
+ $propertyConfig['from_db'] = $propertyOwnerClassMetadata->hasField($propertyName);
- $modelData = $this->persister->getManyToManyData(
- $associationMapping,
- $data['id'],
- $databaseFields,
- $conditions,
- $orderByExpressions
- );
+ $propertyConfig['field_db_name'] =
+ $propertyConfig['from_db'] ? $propertyOwnerClassMetadata->getColumnName($propertyName) : null;
+ }
- $preparedData[$propertyName] = $this->getList(
- $modelData,
- $propertyConfig,
- $propertyScopeConfig,
- $class,
- $propertyName
- );
+ $properties[$propertyName] = $propertyConfig;
+ }
- break;
+ $configuration['properties'] = $properties;
- case ClassMetadata::ONE_TO_MANY:
- $orderByExpressions = isset($associationMapping['orderBy']) ? $associationMapping['orderBy'] : [];
+ return $configuration;
+ }
- $modelData = $this->persister->getOneToManyData(
- $associationMapping,
- $data['id'],
- $databaseFields,
- $conditions,
- $orderByExpressions
- );
+ /**
+ * @param mixed $value
+ * @param string $decodeString
+ * @param TransformEvent $transformEvent
+ * @return mixed
+ * @throws DataTransformerNotExists
+ */
+ protected function decode($value, string $decodeString, TransformEvent $transformEvent)
+ {
+ $dataTransformerNames = $this->dataSchemaService->parseDecodeString($decodeString);
- $preparedData[$propertyName] = $this->getList(
- $modelData,
- $propertyConfig,
- $propertyScopeConfig,
- $class,
- $propertyName
- );
+ foreach ($dataTransformerNames as $dataTransformerName) {
+ $transformer = $this->dataSchemaService->getDataTransformer($dataTransformerName);
+ $value = $transformer->transform($value, $transformEvent);
+ }
- break;
+ return $value;
+ }
- case ClassMetadata::MANY_TO_ONE:
- $modelData = $this->persister->getManyToOneData($associationMapping, $data['id'], $databaseFields, $conditions);
- $preparedData[$propertyName] = $this->getData(
- $modelData,
- $propertyConfig,
- $propertyScopeConfig,
- $class,
- $propertyName,
- null
- );
+ /**
+ * @param array $data
+ * @param array $config
+ * @param array|null $scopeConfig
+ * @return array
+ * @throws MappingException|InvalidConfigurationException
+ */
+ private function fetchMissingPropertiesRecursive(array $data, array $config, array $scopeConfig = null): array
+ {
+ $class = $this->getDataClassName($config, $data);
+ $metadata = $this->dataSchemaService->getClassMetadata($class);
- break;
+ $result = $data + [];
- case ClassMetadata::ONE_TO_ONE:
- $modelData = $this->persister->getOneToOneData($associationMapping, $data['id'], $databaseFields, $conditions);
- $preparedData[$propertyName] = $this->getData(
- $modelData,
- $propertyConfig,
- $propertyScopeConfig,
- $class,
- $propertyName,
- null
- );
+ foreach ($config['properties'] as $propertyName => $propertyConfig) {
+ $propertyScopeConfig = $scopeConfig[$propertyName] ?? [];
+
+ if ($propertyConfig['class'] && $propertyConfig['properties']) {
- break;
+ if (!$metadata->hasAssociation($propertyName)) {
+ continue;
}
- continue;
+ $value = null;
+
+ if (array_key_exists($propertyName, $data)) {
+ $value = $data[$propertyName];
+
+ if (is_array($value)) {
+ if ($this->isIterablePropertyType($propertyConfig['type'])) {
+ $value = array_map(
+ function ($itemData) use ($propertyConfig, $propertyScopeConfig) {
+ return $this->fetchMissingPropertiesRecursive(
+ $itemData,
+ $propertyConfig,
+ $propertyScopeConfig
+ );
+ },
+ $value
+ );
+ } else {
+ $value =
+ $this->fetchMissingPropertiesRecursive($value, $propertyConfig, $propertyScopeConfig);
+ }
+ }
+ } else {
+ $associationMapping = $metadata->getAssociationMapping($propertyName);
+ $databaseFields = $this->dataSchemaService->getDatabaseFields(
+ $propertyConfig,
+ $propertyScopeConfig
+ );
+ $conditions = $propertyConfig['conditions'];
+ $orderByExpressions = $associationMapping['orderBy'] ?? [];
+
+ switch ($associationMapping['type']) {
+ case ClassMetadata::MANY_TO_MANY:
+
+ $modelData = $this->persister->getManyToManyData(
+ $associationMapping,
+ $data['id'],
+ $databaseFields,
+ $conditions,
+ $orderByExpressions
+ );
+
+ $value = array_map(
+ function ($itemData) use ($propertyConfig, $propertyScopeConfig) {
+ return $this->fetchMissingPropertiesRecursive(
+ $itemData,
+ $propertyConfig,
+ $propertyScopeConfig
+ );
+ },
+ $modelData
+ );
+
+ break;
+
+ case ClassMetadata::ONE_TO_MANY:
+ $modelData = $this->persister->getOneToManyData(
+ $associationMapping,
+ $data['id'],
+ $databaseFields,
+ $conditions,
+ $orderByExpressions
+ );
+
+ $value = array_map(
+ function ($itemData) use ($propertyConfig, $propertyScopeConfig) {
+ return $this->fetchMissingPropertiesRecursive(
+ $itemData,
+ $propertyConfig,
+ $propertyScopeConfig
+ );
+ },
+ $modelData
+ );
+
+ break;
+
+ case ClassMetadata::MANY_TO_ONE:
+ $modelData = $this->persister->getManyToOneData(
+ $associationMapping,
+ $data['id'],
+ $databaseFields,
+ $conditions
+ );
+
+ $value = $this->fetchMissingPropertiesRecursive(
+ $modelData,
+ $propertyConfig,
+ $propertyScopeConfig
+ );
+
+ break;
+
+ case ClassMetadata::ONE_TO_ONE:
+ $modelData = $this->persister->getOneToOneData(
+ $associationMapping,
+ $data['id'],
+ $databaseFields,
+ $conditions
+ );
+
+ $value = $this->fetchMissingPropertiesRecursive(
+ $modelData,
+ $propertyConfig,
+ $propertyScopeConfig
+ );
+
+ break;
+ }
+ }
+
+ $result[$propertyName] = $value;
+
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param array $data
+ * @param array $config
+ * @param array|null $scopeConfig
+ * @param string|null $parentClassName
+ * @param string|null $parentPropertyName
+ * @return array
+ * @throws DataTransformerNotExists
+ */
+ private function modifyPropertiesRecursive(array $data,
+ array $config,
+ array $scopeConfig = null,
+ string $parentClassName = null,
+ string $parentPropertyName = null): array
+ {
+ $class = $this->getDataClassName($config, $data);
+
+ $result = [];
+
+ foreach ($config['properties'] as $propertyName => $propertyConfig) {
+ $value = null;
+ $propertyScopeConfig = $scopeConfig[$propertyName] ?? [];
+
+ if (array_key_exists($propertyName, $data)) {
+ $value = $data[$propertyName];
+
+ } elseif ($propertyConfig['source'] && isset($data[$propertyConfig['source']])) {
+ $value = $data[$propertyConfig['source']];
} else {
$value = null;
@@ -303,42 +498,46 @@ public function getData(array $data, array $config = null, array $scopeConfig =
if (is_array($value)) {
if (!array_key_exists('type', $propertyConfig)) {
- throw new \RuntimeException('Option "type" must be defined.');
+ throw new \RuntimeException('Property "type" must be defined.');
}
- if ($propertyConfig['type'] == 'entity') {
+ if ($propertyConfig['type'] === 'entity') {
if (!$this->isOnlyNullInArray($value)) {
- $preparedData[$propertyName] = $this->getData(
+ $value = $this->modifyPropertiesRecursive(
$value,
$propertyConfig,
$propertyScopeConfig,
$class,
- $propertyName,
- null
+ $propertyName
);
- } else {
- if (!$config['filter_null_values']) {
- $preparedData[$propertyName] = null;
- }
+ } else if (!$config['filter_null_values']) {
+ $value = null;
}
- continue;
-
- } elseif ($propertyConfig['type'] == 'collection') {
- $preparedData[$propertyName] = $this->getList(
- $value,
- $propertyConfig,
- $propertyScopeConfig,
- $class,
- $propertyName
+ } elseif ($propertyConfig['type'] === 'collection') {
+ $value = array_map(
+ function ($itemData) use (
+ $propertyConfig,
+ $propertyScopeConfig,
+ $class,
+ $propertyName
+ ) {
+ return $this->modifyPropertiesRecursive(
+ $itemData,
+ $propertyConfig,
+ $propertyScopeConfig,
+ $class,
+ $propertyName
+ );
+ },
+ $value
);
- continue;
}
}
- if (isset($propertyConfig['decode'])) {
+ if ($propertyConfig['decode']) {
$transformEvent = new TransformEvent(
$class,
$propertyName,
@@ -349,6 +548,7 @@ public function getData(array $data, array $config = null, array $scopeConfig =
$this->objectHydrator,
$this->dataSchemaFactory
);
+
$value = $this->decode($value, $propertyConfig['decode'], $transformEvent);
if (is_array($value) && $propertyScopeConfig) {
@@ -359,371 +559,95 @@ public function getData(array $data, array $config = null, array $scopeConfig =
}
}
- $preparedData[$propertyName] = $value;
- }
-
- return $preparedData;
- }
+ if ($value === null) {
+ if ($this->isIterablePropertyType($propertyConfig['type'])) {
+ $value = [];
- /**
- * @param array $list
- * @param array $config
- * @param array $scopeConfig
- * @param string $parentClassName
- * @param string $parentPropertyName
- * @return array
- */
- public function getList(array $list, array $config = null, array $scopeConfig = null, $parentClassName = null, $parentPropertyName = null)
- {
- if ($config === null) {
- $config = $this->configuration;
- }
-
- if ($scopeConfig === null) {
- $scopeConfig = $this->scopeConfig;
- }
+ } elseif ($config['filter_null_values']) {
+ continue;
+ }
+ }
- foreach ($list as $key => $value) {
- $list[$key] = $this->getData(
- $value,
- $config,
- $scopeConfig,
- $parentClassName,
- $parentPropertyName,
- null
- );
+ $result[$propertyName] = $value;
}
- return $list;
+ return $result;
}
/**
- * @param array $configuration
- * @param string $class
- * @param array $scopeConfig
* @return array
- * @throws InvalidConfigurationException
- * @throws \Doctrine\ORM\Mapping\MappingException
*/
- protected function prepareConfiguration(array $configuration, $class = null, array $scopeConfig = null)
+ public function getConfiguration(): array
{
- $classMetadata = $class ? $this->getClassMetadata($class) : null;
-
- // roles
- if (!isset($configuration['roles'])) {
- $configuration['roles'] = [];
- }
-
- $isGranted = $this->isGranted($configuration['roles']);
- if (!$isGranted) {
- return [];
- }
-
- // class
- $configuration['class'] = $class;
- $configuration['discriminatorColumnName'] = null;
- $configuration['discriminatorMap'] = [];
- $isSuperClass = false;
-
- if ($classMetadata instanceof ClassMetadata && $classMetadata->subClasses) {
- $isSuperClass = true;
- $configuration['discriminatorColumnName'] = $classMetadata->discriminatorColumn['name'];
- $configuration['discriminatorMap'] = $classMetadata->discriminatorMap;
- $configuration['tableName'] = $class;
- }
-
- if ($classMetadata instanceof ClassMetadata) {
- $configuration['tableName'] = $classMetadata->getTableName();
- }
-
- // condition
- if (!isset($configuration['conditions'])) {
- $configuration['conditions'] = [];
- }
-
- // filter_null_values
- if (!isset($configuration['filter_null_values'])) {
- $configuration['filter_null_values'] = true;
- }
-
- // inject properties
- if (isset($configuration['schema']) && !$this->withoutAssociations) {
- $configuration = $this->injectDataSchema($configuration['schema'], $configuration);
- }
-
- $selects = $configuration['query']['selects'] ?? [];
-
- if (isset($configuration['properties'])) {
- $properties = $configuration['properties'];
-
- // Set ids
- $identifierFieldNames = $classMetadata instanceof ClassMetadata ?
- $classMetadata->getIdentifierFieldNames() :
- []
- ;
-
- foreach ($properties as $propertyName => $propertyConfig) {
- foreach ($identifierFieldNames as $idName) {
- if (!array_key_exists($idName, $properties)) {
- $properties[$idName] = ['hidden' => true];
- $configuration['properties'][$idName] = $properties[$idName];
- }
- }
- }
-
- $validProperties = array_keys($selects);
-
- foreach ($properties as $propertyName => $propertyConfig) {
-
- // check superclass field requirements
- if ($isSuperClass) {
- $this->validateSuperclassProperty($configuration, $propertyName, $classMetadata, $validProperties);
- }
-
- // Set default discriminator value for property
- if (!isset($configuration['properties'][$propertyName]['discriminator'])) {
- $configuration['properties'][$propertyName]['discriminator'] = null;
- }
- $propertyConfig = $configuration['properties'][$propertyName]; // update $propertyConfig
-
- // If has subclasses
- $hasPropertyClassMetadata =
- $propertyConfig['discriminator'] &&
- isset($configuration['discriminatorMap'][$propertyConfig['discriminator']])
- ;
-
- $propertyClassMetadata = $classMetadata;
- if ($hasPropertyClassMetadata) {
- $propertyClass = $configuration['discriminatorMap'][$propertyConfig['discriminator']];
- $propertyClassMetadata = $this->getClassMetadata($propertyClass);
- }
-
- $isAssociationField = $propertyClassMetadata instanceof ClassMetadata ?
- $propertyClassMetadata->hasAssociation($propertyName) :
- false
- ;
-
- $isRemove =
- $scopeConfig !== null &&
- !in_array($propertyName, $identifierFieldNames) &&
- empty($propertyConfig['hidden']) &&
- !array_key_exists($propertyName, $scopeConfig)
- ;
-
- // if without associations
- $isRemove = $isRemove || ($this->withoutAssociations && $isAssociationField);
-
- if ($isRemove) {
- unset($configuration['properties'][$propertyName]);
- continue;
- }
-
- // set default description
- if (empty($propertyConfig['description']) && $propertyClassMetadata && $propertyClassMetadata->hasField($propertyName)) {
- $fieldMapping = $propertyClassMetadata->getFieldMapping($propertyName);
- $description = isset($fieldMapping['options']['comment']) ? $fieldMapping['options']['comment'] : null;
-
- $configuration['properties'][$propertyName]['description'] = $description;
- }
-
- $isNestedField =
- isset($propertyConfig['properties']) ||
- isset($propertyConfig['schema'])
- ;
-
- if ($isNestedField) {
- if ($propertyConfig['discriminator'] && isset($propertyConfig['join']) && $propertyConfig['join'] != 'none') {
- throw new InvalidConfigurationException($configuration, 'The join type cannot be other than "none" if the discriminator is defined.');
- }
-
- $class = isset($propertyConfig['class']) ?
- $propertyConfig['class'] :
- ($propertyClassMetadata instanceof ClassMetadata ?
- ($propertyClassMetadata->hasAssociation($propertyName) ? $propertyClassMetadata->getAssociationTargetClass($propertyName) : null) :
- null
- )
- ;
-
- $preparedConfiguration = $this->prepareConfiguration($propertyConfig, $class, $scopeConfig[$propertyName]);
- $configuration['properties'][$propertyName] = $preparedConfiguration;
-
- // define type by association mapping
- if (
- $preparedConfiguration &&
- $propertyClassMetadata instanceof ClassMetadata &&
- $propertyClassMetadata->hasAssociation($propertyName)
- ) {
- $associationMapping = $propertyClassMetadata->getAssociationMapping($propertyName);
- $associationType = $associationMapping['type'];
-
- $type = in_array($associationType, [ClassMetadata::ONE_TO_MANY, ClassMetadata::MANY_TO_MANY]) ? 'collection' : 'entity';
- $configuration['properties'][$propertyName]['type'] = $type;
- }
-
- } else {
- if (!isset($propertyConfig['type']) && $propertyClassMetadata instanceof ClassMetadata) {
- $configuration['properties'][$propertyName]['type'] = $propertyClassMetadata->getTypeOfField($propertyName);
- }
-
- $configuration['properties'][$propertyName]['from_db'] =
- $propertyClassMetadata instanceof ClassMetadata &&
- (bool)$propertyClassMetadata->getTypeOfField($propertyName)
- ;
-
- $configuration['properties'][$propertyName]['field_db_name'] = $configuration['properties'][$propertyName]['from_db'] ?
- $propertyClassMetadata->getColumnName($propertyName) :
- null
- ;
- }
- }
- }
-
- return $configuration;
+ return $this->configuration;
}
/**
- * Checks what subclasses properties configurations have discriminator definition
- *
- * @param array $configuration
- * @param string $propertyName
- * @param ClassMetadata|null $classMetadata
- * @param array $validProperties
+ * @param array $data
+ * @param array|null $config
+ * @param array|null $scopeConfig
+ * @param array|null $defaultData
+ * @return array
* @throws InvalidConfigurationException
+ * @throws MappingException|DataTransformerNotExists
*/
- protected function validateSuperclassProperty(array $configuration, string $propertyName, ?ClassMetadata $classMetadata, array $validProperties): void
+ public function getData(array $data,
+ array $config = null,
+ array $scopeConfig = null,
+ ?array $defaultData = []): array
{
- if (!isset($configuration['properties'])) {
- throw new \InvalidArgumentException('Configuration should have properties property');
- }
+ $this->dataSchemaService->startStopwatch('getData');
- $properties = $configuration['properties'];
- $valid = false;
- $depth = 0;
- $propertiesStack = [];
-
- do {
- $propertyConfig = $properties[$propertyName];
- $propertiesStack[] = $propertyName;
-
- if (in_array($propertyName, $validProperties)) {
- $valid = true;
- break;
- }
+ $config = $config ?? $this->configuration;
+ $scopeConfig = $scopeConfig ?? $this->scopeConfig;
- $isClassField = $classMetadata instanceof ClassMetadata
- && (
- $classMetadata->hasField($propertyName)
- || $classMetadata->hasAssociation($propertyName)
- );
-
- if (isset($propertyConfig['discriminator']) || $isClassField) {
- $validProperties[] = $propertyName;
- $valid = true;
- break;
- }
-
- if (++$depth > 10) {
- $propertiesStackString = implode(' > ', $propertiesStack);
- throw new InvalidConfigurationException($configuration, "Maximum depth exceeded \"$propertiesStackString\"");
- }
-
- } while ($propertyName = $propertyConfig['source'] ?? null);
-
- if (!$valid) {
- throw new InvalidConfigurationException($configuration,
- "Property \"$propertyName\" should have \"discriminator\" definition"
- );
+ if ($config !== $this->configuration || $scopeConfig !== $this->scopeConfig) {
+ $config = $this->dataSchemaFilter->filter($config, $scopeConfig, $this->nestingDepth);
}
- }
- protected function getScopedData(array $data, array $scope): array
- {
- $scopedData = [];
-
- foreach ($data as $key => $value) {
- if (array_key_exists($key, $scope)) {
- if (is_array($value) && $scope[$key]) {
- $scopedData[$key] = $this->getScopedData($value, $scope[$key]);
-
- } else {
- $scopedData[$key] = $value;
- }
- }
+ if (!$data) {
+ return $defaultData;
}
- return $scopedData;
- }
-
- /**
- * @param string $class
- * @return ClassMetadata
- */
- protected function getClassMetadata($class)
- {
- if (!isset($this->classMetadataCache[$class])) {
- $classMetadata = $this->doctrine->getManager()->getClassMetadata($class);
-
- $this->classMetadataCache[$class] = $classMetadata;
+ if (!$config['properties']) {
+ return $defaultData;
}
- return $this->classMetadataCache[$class];
- }
-
- /**
- * @param mixed $value
- * @param string $decodeString
- * @param TransformEvent $transformEvent
- * @return mixed
- */
- protected function decode($value, $decodeString, TransformEvent $transformEvent)
- {
- $dataTransformerNames = explode('|', $decodeString);
- $dataTransformerNames = array_map('trim', $dataTransformerNames);
+ $fetchedData = $this->fetchMissingPropertiesRecursive($data, $config, $scopeConfig);
- foreach ($dataTransformerNames as $dataTransformerName) {
- $hasDataTransformer = $this->dataTransformerRegistry->has($dataTransformerName);
+ $modifiedData = $this->modifyPropertiesRecursive($fetchedData, $config, $scopeConfig);
- if ($hasDataTransformer) {
- $transformer = $this->dataTransformerRegistry->get($dataTransformerName);
- $value = $transformer->transform($value, $transformEvent);
- }
- }
+ $this->dataSchemaService->stopStopwatch('getData');
- return $value;
+ return $modifiedData;
}
/**
- * @param array $entityConfig
- * @param array $scopeConfig
+ * @param array $list
+ * @param array|null $config
+ * @param array|null $scopeConfig
* @return array
+ * @throws MappingException|InvalidConfigurationException
+ * @throws DataTransformerNotExists
*/
- public static function getDatabaseFields(array $entityConfig, array $scopeConfig = null)
+ public function getList(array $list, array $config = null, array $scopeConfig = null): array
{
- $properties = $entityConfig['properties'];
- $entityClass = $entityConfig['class'];
- $discriminatorMap = $entityConfig['discriminatorMap'] ?? null;
- $databaseFields = [];
- foreach ($properties as $propertyName => $propertyData) {
- if (isset($propertyData['discriminator']) && $discriminatorMap && $discriminatorMap[$propertyData['discriminator']] !== $entityClass) {
- continue;
- }
- if ($scopeConfig && !array_key_exists($propertyName, $scopeConfig)) {
- continue;
- }
+ $this->dataSchemaService->startStopwatch('getList');
- while (isset($propertyData['source']) && $propertyData['source'] !== $propertyName) {
- $propertyName = $propertyData['source'];
- $propertyData = $properties[$propertyName];
- }
-
- $isValid = (isset($propertyData['from_db']) && $propertyData['from_db']);
+ foreach ($list as $key => $value) {
+ $list[$key] = $this->getData(
+ $value,
+ $config,
+ $scopeConfig,
+ null
+ );
- if ($isValid && !in_array($propertyName, $databaseFields)) {
- $databaseFields[] = $propertyName;
- }
+ $this->dataSchemaService->lapStopwatch('getList');
}
- return $databaseFields;
+ $this->dataSchemaService->stopStopwatch('getList');
+
+ return $list;
}
/**
@@ -731,36 +655,14 @@ public static function getDatabaseFields(array $entityConfig, array $scopeConfig
*/
public function getQuerySelects(): array
{
- return isset($this->configuration['query']['selects']) ? $this->configuration['query']['selects'] : [];
- }
-
- /**
- * @param string $propertyName
- * @return bool
- */
- public function hasProperty(string $propertyName): bool
- {
- return $this->getPropertyConfiguration($propertyName) !== null;
- }
-
- /**
- * @param string $propertyName
- * @return bool
- */
- public function hasPropertyInDb(string $propertyName): bool
- {
- $propertyConfiguration = $this->getPropertyConfiguration($propertyName);
-
- return $propertyConfiguration !== null &&
- isset($propertyConfiguration['from_db']) && $propertyConfiguration['from_db']
- ;
+ return $this->configuration['query']['selects'] ?? [];
}
/**
* @param string $propertyName
* @return array|null
*/
- public function getPropertyConfiguration(string $propertyName):? array
+ public function getPropertyConfiguration(string $propertyName): ?array
{
$propertyConfiguration = $this->configuration;
@@ -781,70 +683,64 @@ public function getPropertyConfiguration(string $propertyName):? array
*/
public function getHydrationMode()
{
- return isset($this->configuration['hydration_mode']) ? $this->configuration['hydration_mode'] : $this->defaultHydratorMode;
+ return $this->configuration['hydration_mode'] ?? $this->defaultHydratorMode;
}
/**
- * @param array $roles
- * @return bool
+ * @param array $data
+ * @param array $scope
+ * @return array
*/
- protected function isGranted(array $roles)
+ protected function getScopedData(array $data, array $scope): array
{
- if (empty($roles)) {
- return true;
- }
+ $scopedData = [];
+
+ foreach ($data as $key => $value) {
+ if (array_key_exists($key, $scope)) {
+ if (is_array($value) && $scope[$key]) {
+ $scopedData[$key] = $this->getScopedData($value, $scope[$key]);
- foreach ($roles as $role) {
- if ($this->authorizationChecker->isGranted($role)) {
- return true;
+ } else {
+ $scopedData[$key] = $value;
+ }
}
}
- return false;
+ return $scopedData;
}
/**
- * @param string $condition
- * @param string $alias
- * @param UserInterface $user
- * @return string
+ * @param string|null $type
+ * @return bool
*/
- public function conditionPlaceholder($condition, $alias, UserInterface $user = null)
+ private function isIterablePropertyType(?string $type): bool
{
- return $this->placeholder->condition($condition, $alias, $user);
+ return in_array($type, ['array', 'json_array', 'collection']);
}
/**
- * @param string $dataSchemaFile
- * @param array $configuration
+ * @param string $dataSchemaFile
+ * @param array $configuration
+ * @param int $nestingDepth
+ * @param array|null $scopeConfig
* @return array
+ * @throws InvalidConfigurationException
+ * @throws MappingException
*/
- private function injectDataSchema($dataSchemaFile, array $configuration)
+ private function getNestedDataSchemaConfiguration(string $dataSchemaFile,
+ array $configuration,
+ int $nestingDepth,
+ array $scopeConfig = null): array
{
- $dataSchema = $this->dataSchemaFactory->createDataSchema($dataSchemaFile, null, true);
- $injectedConfiguration = $dataSchema->getConfiguration();
-
- // inject properties (save source property order)
- if (isset($injectedConfiguration['properties'])) {
- $injectedProperties = $injectedConfiguration['properties'];
-
- $properties = isset($configuration['properties']) ? $configuration['properties'] : [];
- foreach ($properties as $propertyName => $propertyConfig) {
- $injectedProperties[$propertyName] = $propertyConfig;
- }
- $configuration['properties'] = $injectedProperties;
- unset($injectedConfiguration['properties']);
- }
-
- // inject rest configuration parameters
- foreach ($injectedConfiguration as $key => $value) {
- if (!array_key_exists($key, $configuration)) {
- $configuration[$key] = $value;
- }
- }
+ $dataSchema = $this->dataSchemaFactory->createNestedDataSchema(
+ $dataSchemaFile,
+ $configuration,
+ $scopeConfig,
+ $nestingDepth
+ );
- return $configuration;
+ return $dataSchema->getConfiguration();
}
/**
@@ -861,4 +757,21 @@ private function isOnlyNullInArray(array $array): bool
return true;
}
+
+ /**
+ * @param array $config
+ * @param array $data
+ * @return string
+ */
+ private function getDataClassName(array $config, array $data): string
+ {
+ $class = $config['class'];
+
+ if ($config['discriminatorMap'] && isset($data[$config['discriminatorColumnName']])) {
+ $discriminator = $data[$config['discriminatorColumnName']];
+ $class = $config['discriminatorMap'][$discriminator];
+ }
+
+ return $class;
+ }
}
diff --git a/DataSchema/DataSchemaFactory.php b/DataSchema/DataSchemaFactory.php
old mode 100644
new mode 100755
index b126bc0..a0e2c95
--- a/DataSchema/DataSchemaFactory.php
+++ b/DataSchema/DataSchemaFactory.php
@@ -11,154 +11,158 @@
namespace Glavweb\DataSchemaBundle\DataSchema;
-use Doctrine\Bundle\DoctrineBundle\Registry;
+use Doctrine\ORM\Mapping\MappingException;
use Glavweb\DataSchemaBundle\DataSchema\Persister\PersisterFactory;
-use Glavweb\DataSchemaBundle\DataTransformer\DataTransformerRegistry;
+use Glavweb\DataSchemaBundle\Exception\DataSchema\InvalidConfigurationException;
use Glavweb\DataSchemaBundle\Hydrator\Doctrine\ObjectHydrator;
-use Glavweb\DataSchemaBundle\Loader\Yaml\DataSchemaYamlLoader;
-use Glavweb\DataSchemaBundle\Loader\Yaml\ScopeYamlLoader;
-use Symfony\Component\Config\FileLocator;
-use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Glavweb\DataSchemaBundle\Service\DataSchemaFilter;
+use Glavweb\DataSchemaBundle\Service\DataSchemaService;
+use Glavweb\DataSchemaBundle\Service\DataSchemaValidator;
+use Glavweb\DataSchemaBundle\Util\Utils;
/**
* Class DataSchemaFactory
*
- * @author Andrey Nilov
+ * @author Andrey Nilov
* @package Glavweb\DataSchemaBundle
*/
class DataSchemaFactory
{
- /**
- * @var Registry
- */
- private $doctrine;
/**
- * @var DataTransformerRegistry
+ * @var PersisterFactory
*/
- private $dataTransformerRegistry;
+ private $persisterFactory;
/**
- * @var PersisterFactory
+ * @var string
*/
- private $persisterFactory;
+ private $defaultHydratorMode;
/**
- * @var AuthorizationCheckerInterface
+ * @var Placeholder
*/
- private $authorizationChecker;
+ private $placeholder;
/**
- * @var string
+ * @var ObjectHydrator
*/
- private $dataSchemaDir;
+ private $objectHydrator;
/**
- * @var string
+ * @var int
*/
- private $scopeDir;
+ private $nestingDepth;
/**
- * @var string
+ * @var DataSchemaService
*/
- private $defaultHydratorMode;
+ private $dataSchemaService;
/**
- * @var Placeholder
+ * @var DataSchemaFilter
*/
- private $placeholder;
+ private $dataSchemaFilter;
/**
- * @var ObjectHydrator
+ * @var DataSchemaValidator
*/
- private $objectHydrator;
+ private $dataSchemaValidator;
/**
* DataSchema constructor.
*
- * @param Registry $doctrine
- * @param DataTransformerRegistry $dataTransformerRegistry
- * @param PersisterFactory $persisterFactory
- * @param AuthorizationCheckerInterface $authorizationChecker
- * @param Placeholder $placeholder
- * @param ObjectHydrator $objectHydrator
- * @param string $dataSchemaDir
- * @param string $scopeDir
- * @param string $defaultHydratorMode
+ * @param DataSchemaService $dataSchemaService
+ * @param DataSchemaFilter $dataSchemaFilter
+ * @param DataSchemaValidator $dataSchemaValidator
+ * @param PersisterFactory $persisterFactory
+ * @param Placeholder $placeholder
+ * @param ObjectHydrator $objectHydrator
+ * @param int $nestingDepth
+ * @param string|null $defaultHydratorMode
*/
- public function __construct(
- Registry $doctrine,
- DataTransformerRegistry $dataTransformerRegistry,
- PersisterFactory $persisterFactory,
- AuthorizationCheckerInterface $authorizationChecker,
- Placeholder $placeholder,
- ObjectHydrator $objectHydrator,
- string $dataSchemaDir,
- string $scopeDir,
- $defaultHydratorMode = null
- ){
- $this->doctrine = $doctrine;
- $this->dataTransformerRegistry = $dataTransformerRegistry;
- $this->persisterFactory = $persisterFactory;
- $this->authorizationChecker = $authorizationChecker;
- $this->placeholder = $placeholder;
- $this->objectHydrator = $objectHydrator;
- $this->dataSchemaDir = $dataSchemaDir;
- $this->scopeDir = $scopeDir;
- $this->defaultHydratorMode = $defaultHydratorMode;
+ public function __construct(DataSchemaService $dataSchemaService,
+ DataSchemaFilter $dataSchemaFilter,
+ DataSchemaValidator $dataSchemaValidator,
+ PersisterFactory $persisterFactory,
+ Placeholder $placeholder,
+ ObjectHydrator $objectHydrator,
+ int $nestingDepth,
+ string $defaultHydratorMode = null)
+ {
+ $this->dataSchemaService = $dataSchemaService;
+ $this->dataSchemaFilter = $dataSchemaFilter;
+ $this->dataSchemaValidator = $dataSchemaValidator;
+ $this->persisterFactory = $persisterFactory;
+ $this->placeholder = $placeholder;
+ $this->objectHydrator = $objectHydrator;
+ $this->nestingDepth = $nestingDepth;
+ $this->defaultHydratorMode = $defaultHydratorMode;
}
/**
- * @param string $dataSchemaFile
- * @param string $scopeFile
- * @param bool $withoutInheritance
+ * @param string $dataSchemaFile
+ * @param string|null $scopeFile
* @return DataSchema
+ * @throws InvalidConfigurationException|MappingException
*/
- public function createDataSchema($dataSchemaFile, $scopeFile = null, $withoutInheritance = false)
+ public function createDataSchema(string $dataSchemaFile, string $scopeFile = null): DataSchema
{
- $dataSchemaConfig = $this->getDataSchemaConfig($dataSchemaFile);
+ $dataSchemaConfig = $this->dataSchemaService->getConfigurationFromFile($dataSchemaFile);
+
+ $this->dataSchemaValidator->validate($dataSchemaConfig, $this->nestingDepth);
$scopeConfig = null;
if ($scopeFile) {
- $scopeConfig = $this->getScopeConfig($scopeFile);
+ $scopeConfig = $this->dataSchemaService->loadScopeConfiguration($scopeFile);
}
return new DataSchema(
$this,
- $this->doctrine,
- $this->dataTransformerRegistry,
+ $this->dataSchemaService,
+ $this->dataSchemaFilter,
$this->persisterFactory,
- $this->authorizationChecker,
$this->placeholder,
$this->objectHydrator,
$dataSchemaConfig,
$scopeConfig,
- $withoutInheritance,
+ $this->nestingDepth,
$this->defaultHydratorMode
);
}
/**
- * @param string $dataSchemaFile
- * @return array
+ * @param string $dataSchemaFile
+ * @param array $configuration
+ * @param null $scopeConfig
+ * @param int|null $nestingDepth
+ * @return DataSchema
+ * @throws MappingException
+ * @throws InvalidConfigurationException
*/
- public function getDataSchemaConfig($dataSchemaFile)
+ public function createNestedDataSchema(string $dataSchemaFile,
+ array $configuration,
+ $scopeConfig = null,
+ int $nestingDepth = null): DataSchema
{
- $dataSchemaLoader = new DataSchemaYamlLoader(new FileLocator($this->dataSchemaDir));
- $dataSchemaLoader->load($dataSchemaFile);
+ $dataSchemaConfig = $this->dataSchemaService->getConfigurationFromFile($dataSchemaFile);
- return $dataSchemaLoader->getConfiguration();
- }
+ $this->dataSchemaValidator->validate($dataSchemaConfig, $this->nestingDepth);
- /**
- * @param string $scopeFile
- * @return array
- */
- private function getScopeConfig($scopeFile)
- {
- $scopeLoader = new ScopeYamlLoader(new FileLocator($this->scopeDir));
- $scopeLoader->load($scopeFile);
+ $mergedConfig = Utils::arrayDeepMerge($dataSchemaConfig, $configuration);
- return $scopeLoader->getConfiguration();
+ return new DataSchema(
+ $this,
+ $this->dataSchemaService,
+ $this->dataSchemaFilter,
+ $this->persisterFactory,
+ $this->placeholder,
+ $this->objectHydrator,
+ $mergedConfig,
+ $scopeConfig,
+ $nestingDepth,
+ $this->defaultHydratorMode
+ );
}
+
}
\ No newline at end of file
diff --git a/DataSchema/Persister/EntityPersister.php b/DataSchema/Persister/EntityPersister.php
old mode 100644
new mode 100755
index 8d26e96..be563c7
--- a/DataSchema/Persister/EntityPersister.php
+++ b/DataSchema/Persister/EntityPersister.php
@@ -14,13 +14,14 @@
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query;
+use Doctrine\ORM\Query\Expr\Join;
use Glavweb\DataSchemaBundle\DataSchema\DataSchema;
use Glavweb\DataSchemaBundle\Exception\Persister\InvalidQueryException;
/**
* Class EntityPersister
*
- * @author Andrey Nilov
+ * @author Andrey Nilov
* @package Glavweb\DataSchemaBundle
*/
class EntityPersister implements PersisterInterface
@@ -61,10 +62,11 @@ public function __construct(Registry $doctrine, DataSchema $dataSchema, $hydrati
* @param array $conditions
* @param array $orderByExpressions
* @return array
+ * @throws InvalidQueryException
*/
public function getManyToManyData(array $associationMapping, $id, array $databaseFields, array $conditions = [], array $orderByExpressions = [])
{
- $query = $this->getQuery($associationMapping, $id, $databaseFields, $conditions, $orderByExpressions);
+ $query = $this->getQuery($associationMapping, $id, false, $databaseFields, $conditions, $orderByExpressions);
return $query->getArrayResult();
}
@@ -76,10 +78,11 @@ public function getManyToManyData(array $associationMapping, $id, array $databas
* @param array $conditions
* @param array $orderByExpressions
* @return array
+ * @throws InvalidQueryException
*/
public function getOneToManyData(array $associationMapping, $id, array $databaseFields, array $conditions = [], array $orderByExpressions = [])
{
- $query = $this->getQuery($associationMapping, $id, $databaseFields, $conditions, $orderByExpressions);
+ $query = $this->getQuery($associationMapping, $id, false, $databaseFields, $conditions, $orderByExpressions);
return $query->getArrayResult();
}
@@ -90,10 +93,12 @@ public function getOneToManyData(array $associationMapping, $id, array $database
* @param array $databaseFields
* @param array $conditions
* @return array
+ * @throws InvalidQueryException
+ * @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getManyToOneData(array $associationMapping, $id, array $databaseFields, array $conditions = [])
{
- $query = $this->getQuery($associationMapping, $id, $databaseFields, $conditions);
+ $query = $this->getQuery($associationMapping, $id, true, $databaseFields, $conditions);
return (array)$query->getOneOrNullResult();
}
@@ -104,10 +109,12 @@ public function getManyToOneData(array $associationMapping, $id, array $database
* @param array $databaseFields
* @param array $conditions
* @return array
+ * @throws InvalidQueryException
+ * @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getOneToOneData(array $associationMapping, $id, array $databaseFields, array $conditions = [])
{
- $query = $this->getQuery($associationMapping, $id, $databaseFields, $conditions);
+ $query = $this->getQuery($associationMapping, $id, true, $databaseFields, $conditions);
return (array)$query->getOneOrNullResult();
}
@@ -115,36 +122,56 @@ public function getOneToOneData(array $associationMapping, $id, array $databaseF
/**
* @param array $associationMapping
* @param mixed $id
+ * @param bool $single
* @param array $databaseFields
* @param array $conditions
* @param array $orderByExpressions
* @return Query
* @throws InvalidQueryException
*/
- protected function getQuery(array $associationMapping, $id, array $databaseFields, array $conditions = [], array $orderByExpressions = [])
+ protected function getQuery(array $associationMapping, $id, bool $single, array $databaseFields, array $conditions = [], array $orderByExpressions = [])
{
/** @var EntityManager $em */
$em = $this->doctrine->getManager();
$targetClass = $associationMapping['targetEntity'];
$joinField = $associationMapping['isOwningSide'] ? $associationMapping['inversedBy'] : $associationMapping['mappedBy'];
- $targetAlias = uniqid('t');
- $sourceAlias = uniqid('s');
+ $targetAlias = uniqid('t', false);
+ $sourceAlias = uniqid('s', false);
+ $qb = $em->createQueryBuilder();
if (!$joinField) {
- throw new InvalidQueryException(sprintf('The join filed part cannot be defined. May be you need configure association mapping for classes "%s" and "%s".',
- $associationMapping['sourceEntity'],
- $targetClass
- ));
- }
+ $sourceClass = $associationMapping['sourceEntity'];
+ $sourceField = $associationMapping['fieldName'];
+ $associationOperator = $single ? '=' : 'MEMBER OF';
+
+ if (!$sourceField) {
+ throw new InvalidQueryException(
+ sprintf(
+ 'The join filed part cannot be defined. May be you need configure association mapping for classes "%s" and "%s".',
+ $associationMapping['sourceEntity'],
+ $targetClass
+ )
+ );
+ }
- $qb = $em->createQueryBuilder()
- ->select(sprintf('PARTIAL %s.{%s}', $targetAlias, implode(',', $databaseFields)))
- ->from($targetClass, $targetAlias)
- ->join(sprintf('%s.%s', $targetAlias, $joinField), $sourceAlias)
- ->where($sourceAlias . '.id = :sourceId')
- ->setParameter('sourceId', $id)
- ;
+ $qb
+ ->select(sprintf('PARTIAL %s.{%s}', $targetAlias, implode(',', $databaseFields)))
+ ->from($targetClass, $targetAlias)
+ ->join($sourceClass, $sourceAlias, Join::WITH,
+ sprintf('%s %s %s.%s', $targetAlias, $associationOperator, $sourceAlias, $sourceField)
+ )
+ ->where($sourceAlias . '.id = :sourceId')
+ ->setParameter('sourceId', $id);
+
+ } else {
+ $qb
+ ->select(sprintf('PARTIAL %s.{%s}', $targetAlias, implode(',', $databaseFields)))
+ ->from($targetClass, $targetAlias)
+ ->join(sprintf('%s.%s', $targetAlias, $joinField), $sourceAlias)
+ ->where($sourceAlias . '.id = :sourceId')
+ ->setParameter('sourceId', $id);
+ }
foreach ($conditions as $condition) {
$preparedCondition = $this->dataSchema->conditionPlaceholder($condition, $targetAlias);
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
old mode 100644
new mode 100755
index 7947ce7..30705c6
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -40,6 +40,7 @@ public function getConfigTreeBuilder()
->arrayNode('data_schema')
->children()
->scalarNode('dir')->end()
+ ->integerNode('max_nesting_depth')->defaultValue(10)->min(1)->end()
->end()
->end()
->arrayNode('scope')
diff --git a/DependencyInjection/GlavwebDataSchemaExtension.php b/DependencyInjection/GlavwebDataSchemaExtension.php
old mode 100644
new mode 100755
index 723fdfd..7a956da
--- a/DependencyInjection/GlavwebDataSchemaExtension.php
+++ b/DependencyInjection/GlavwebDataSchemaExtension.php
@@ -11,13 +11,13 @@
namespace Glavweb\DataSchemaBundle\DependencyInjection;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
-use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
- * Class GlavwebDatagridExtension
+ * Class GlavwebDataSchemaExtension
*
* This is the class that loads and manages your bundle configuration
*
@@ -41,6 +41,7 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter('glavweb_data_schema.default_hydrator_mode', $config['default_hydrator_mode']);
$container->setParameter('glavweb_data_schema.data_schema_dir', $config['data_schema']['dir']);
+ $container->setParameter('glavweb_data_schema.data_schema_max_nesting_depth', $config['data_schema']['max_nesting_depth']);
$container->setParameter('glavweb_data_schema.scope_dir', $config['scope']['dir']);
}
}
diff --git a/Exception/DataSchema/InvalidConfigurationException.php b/Exception/DataSchema/InvalidConfigurationException.php
old mode 100644
new mode 100755
index 476d015..4fd83b4
--- a/Exception/DataSchema/InvalidConfigurationException.php
+++ b/Exception/DataSchema/InvalidConfigurationException.php
@@ -25,17 +25,17 @@ class InvalidConfigurationException extends Exception
/**
* InvalidConfigurationException constructor.
*
- * @param array $configuration
- * @param string $message
- * @param int $code
+ * @param array|null $configuration
+ * @param string $message
+ * @param int $code
* @param Throwable|null $previous
*/
public function __construct(array $configuration = null, $message = "", $code = 0, Throwable $previous = null)
{
- $className = $configuration['class'] ?? null;
+ $schemaName = $configuration['schema'] ?? null;
- if ($className) {
- $message = "$className: $message";
+ if ($schemaName) {
+ $message = "Schema \"$schemaName\": $message";
}
parent::__construct($message, $code, $previous);
diff --git a/Exception/DataSchema/InvalidConfigurationPropertyException.php b/Exception/DataSchema/InvalidConfigurationPropertyException.php
new file mode 100644
index 0000000..72ff076
--- /dev/null
+++ b/Exception/DataSchema/InvalidConfigurationPropertyException.php
@@ -0,0 +1,21 @@
+ $values) {
- if (in_array($namespace, array('imports'))) {
+ if ($namespace === 'imports') {
continue;
}
diff --git a/Loader/Yaml/ScopeYamlLoader.php b/Loader/Yaml/ScopeYamlLoader.php
old mode 100644
new mode 100755
index 0bb0a92..122766f
--- a/Loader/Yaml/ScopeYamlLoader.php
+++ b/Loader/Yaml/ScopeYamlLoader.php
@@ -108,7 +108,7 @@ private function parseImports($content, $file)
private function loadConfiguration(array $content)
{
foreach ($content as $namespace => $values) {
- if (in_array($namespace, array('imports'))) {
+ if ($namespace === 'imports') {
continue;
}
diff --git a/Resources/config/services.yml b/Resources/config/services.yml
old mode 100644
new mode 100755
index b3b145a..86f4f95
--- a/Resources/config/services.yml
+++ b/Resources/config/services.yml
@@ -1,16 +1,39 @@
services:
- glavweb_data_schema.data_schema_factory:
- class: Glavweb\DataSchemaBundle\DataSchema\DataSchemaFactory
+ glavweb_data_schema.service:
+ class: Glavweb\DataSchemaBundle\Service\DataSchemaService
public: true
arguments:
- "@doctrine"
- "@glavweb_data_schema.data_transformer_registry"
- - "@glavweb_data_schema.persister_factory"
+ - "%glavweb_data_schema.data_schema_dir%"
+ - "%glavweb_data_schema.scope_dir%"
+ - "%glavweb_data_schema.data_schema_max_nesting_depth%"
+ - "@?debug.stopwatch"
+
+ glavweb_data_schema.filter:
+ class: Glavweb\DataSchemaBundle\Service\DataSchemaFilter
+ public: true
+ arguments:
+ - "@glavweb_data_schema.service"
- "@security.authorization_checker"
+
+ glavweb_data_schema.validator:
+ class: Glavweb\DataSchemaBundle\Service\DataSchemaValidator
+ public: true
+ arguments:
+ - "@glavweb_data_schema.service"
+
+ glavweb_data_schema.data_schema_factory:
+ class: Glavweb\DataSchemaBundle\DataSchema\DataSchemaFactory
+ public: true
+ arguments:
+ - "@glavweb_data_schema.service"
+ - "@glavweb_data_schema.filter"
+ - "@glavweb_data_schema.validator"
+ - "@glavweb_data_schema.persister_factory"
- "@glavweb_data_schema.placeholder"
- "@glavweb_data_schema.orm_object_hydrator"
- - "%glavweb_data_schema.data_schema_dir%"
- - "%glavweb_data_schema.scope_dir%"
+ - "%glavweb_data_schema.data_schema_max_nesting_depth%"
- "%glavweb_data_schema.default_hydrator_mode%"
glavweb_data_schema.placeholder:
@@ -37,6 +60,11 @@ services:
tags:
- { name: console.command, command: "glavweb:data-schema" }
+ glavweb_data_schema.validate_data_schema_command:
+ class: Glavweb\DataSchemaBundle\Command\ValidateDataSchemaCommand
+ tags:
+ - { name: console.command, command: "glavweb:data-schema:validate" }
+
glavweb_data_schema.generate_scope_command:
class: Glavweb\DataSchemaBundle\Command\GenerateScopeCommand
tags:
diff --git a/Service/DataSchemaFilter.php b/Service/DataSchemaFilter.php
new file mode 100644
index 0000000..bf0ce91
--- /dev/null
+++ b/Service/DataSchemaFilter.php
@@ -0,0 +1,147 @@
+dataSchemaService = $dataSchemaService;
+ $this->authorizationChecker = $authorizationChecker;
+ }
+
+ /**
+ * @param array $config
+ * @param array|null $scopeConfig
+ * @param int $nestingDepth
+ * @return array
+ * @throws InvalidConfigurationException
+ */
+ public function filter(array $config, array $scopeConfig = null, int $nestingDepth = 0): array
+ {
+ if (!$this->isGranted($config['roles'] ?? [])) {
+ return [];
+ }
+
+ $configProperties = $config['properties'];
+ $result = $config + [];
+
+ if ($configProperties) {
+ $class = $config['class'] ?? null;
+ $classMetadata = $class ? $this->dataSchemaService->getClassMetadata($class) : null;
+ $identifierFieldNames =
+ $classMetadata instanceof ClassMetadata ? $classMetadata->getIdentifierFieldNames() : [];
+
+ $properties = [];
+
+ foreach ($configProperties as $propertyName => $propertyConfig) {
+ $propertyScopeConfig = $scopeConfig[$propertyName] ?? null;
+ $isNested = $propertyConfig['schema'] || $propertyConfig['properties'];
+ $isIdentifier = in_array($propertyName, $identifierFieldNames, true);
+ $isHidden = $propertyConfig['hidden'] ?? false;
+ $isInScope = array_key_exists($propertyName, $scopeConfig) || !$scopeConfig;
+
+ if ((!$isInScope && !$isHidden && !$isIdentifier) || (!$isInScope && $isNested && $nestingDepth <= 0)) {
+ continue;
+ }
+
+ $source = $propertyConfig['source'] ?? null;
+
+ if ($source) {
+ $propertySourcesStack = $this->dataSchemaService->getPropertySourcesStack($config, $propertyName);
+ foreach ($propertySourcesStack as [$sourcePropertyName, $sourcePropertyConfig]) {
+ if (!isset($properties[$sourcePropertyName]) && $sourcePropertyConfig) {
+ $sourcePropertyScopeConfig = $scopeConfig[$sourcePropertyName] ?? null;
+ $sourcePropertyConfig = $this->filterProperty(
+ $sourcePropertyConfig,
+ $sourcePropertyScopeConfig,
+ $nestingDepth - 1
+ );
+
+ if ($sourcePropertyConfig) {
+ $properties[$sourcePropertyName] = $sourcePropertyConfig;
+ }
+ }
+ }
+ }
+
+ $propertyConfig = $this->filterProperty(
+ $propertyConfig,
+ $propertyScopeConfig,
+ $nestingDepth - 1
+ );
+
+ if ($propertyConfig) {
+ $properties[$propertyName] = $propertyConfig;
+ }
+ }
+
+ $result['properties'] = $properties;
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param array $roles
+ * @return bool
+ */
+ public function isGranted(array $roles): bool
+ {
+ if (empty($roles)) {
+ return true;
+ }
+
+ foreach ($roles as $role) {
+ if ($this->authorizationChecker->isGranted($role)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param array $propertyConfig
+ * @param array|null $scopeConfig
+ * @param int $nestingDepth
+ * @return array|null
+ * @throws InvalidConfigurationException
+ */
+ private function filterProperty(array $propertyConfig,
+ ?array $scopeConfig,
+ int $nestingDepth): ?array
+ {
+ $isNested = $propertyConfig['schema'] || $propertyConfig['properties'];
+ $sourcePropertyScopeConfig = $scopeConfig ?? null;
+
+ if ($isNested) {
+ return $this->filter(
+ $propertyConfig,
+ $sourcePropertyScopeConfig,
+ $nestingDepth
+ );
+ }
+
+ return $propertyConfig;
+ }
+}
\ No newline at end of file
diff --git a/Service/DataSchemaService.php b/Service/DataSchemaService.php
new file mode 100644
index 0000000..e926df6
--- /dev/null
+++ b/Service/DataSchemaService.php
@@ -0,0 +1,322 @@
+
+ */
+class DataSchemaService
+{
+
+ /**
+ * @var Registry
+ */
+ private $doctrine;
+
+ /**
+ * @var int
+ */
+ private $nestingDepth;
+
+ /**
+ * @var DataTransformerRegistry
+ */
+ private $dataTransformerRegistry;
+
+ /**
+ * @var FileLocator
+ */
+ private $scopeFileLocator;
+
+ /**
+ * @var FileLocator
+ */
+ private $dataSchemaFileLocator;
+
+ /**
+ * @var array
+ */
+ private $dataSchemaConfigCache = [];
+
+ /**
+ * @var Stopwatch|null
+ */
+ private $stopwatch;
+
+ /**
+ * DataSchemaService constructor.
+ */
+ public function __construct(Registry $doctrine,
+ DataTransformerRegistry $dataTransformerRegistry,
+ string $dataSchemaDir,
+ string $scopeDir,
+ int $nestingDepth,
+ ?Stopwatch $stopwatch)
+ {
+ $this->doctrine = $doctrine;
+ $this->dataTransformerRegistry = $dataTransformerRegistry;
+ $this->nestingDepth = $nestingDepth;
+ $this->dataSchemaFileLocator = new FileLocator($dataSchemaDir);
+ $this->scopeFileLocator = new FileLocator($scopeDir);
+ $this->stopwatch = $stopwatch;
+ }
+
+ /**
+ * @param array $configuration
+ * @return array
+ * @throws InvalidConfigurationException
+ */
+ public function processSchemaConfiguration(array $configuration): array
+ {
+ $processor = new Processor();
+
+ $dataSchemaConfiguration = new DataSchemaConfiguration($this->nestingDepth);
+
+ try {
+ return $processor->processConfiguration(
+ $dataSchemaConfiguration,
+ [$configuration]
+ );
+ } catch (\Exception $e) {
+ throw new InvalidConfigurationException($configuration, $e->getMessage());
+ }
+ }
+
+ /**
+ * @param string $dataSchemaFile
+ * @return array
+ */
+ public function loadSchemaConfigurationFromFile(string $dataSchemaFile): array
+ {
+ $dataSchemaLoader = new DataSchemaYamlLoader($this->dataSchemaFileLocator);
+ $dataSchemaLoader->load($dataSchemaFile);
+
+ return $dataSchemaLoader->getConfiguration();
+ }
+
+ /**
+ * @param string $scopeFile
+ * @return array
+ */
+ public function loadScopeConfiguration(string $scopeFile): array
+ {
+ $scopeLoader = new ScopeYamlLoader($this->scopeFileLocator);
+ $scopeLoader->load($scopeFile);
+
+ return $scopeLoader->getConfiguration();
+ }
+
+ /**
+ * @param string $decodeString
+ * @return array dataTransformerNames
+ */
+ public function parseDecodeString(string $decodeString): array
+ {
+ $dataTransformerNames = explode('|', $decodeString);
+
+ return array_map('trim', $dataTransformerNames);
+ }
+
+ /**
+ * @param string $name
+ */
+ public function startStopwatch(string $name): void
+ {
+ if ($this->stopwatch) {
+ $this->stopwatch->start($name, 'GlavwebDataSchemaBundle');
+ }
+ }
+
+ /**
+ * @param string $name
+ */
+ public function stopStopwatch(string $name): void
+ {
+ if ($this->stopwatch) {
+ $this->stopwatch->stop($name);
+ }
+ }
+
+ /**
+ * @param string $name
+ */
+ public function lapStopwatch(string $name): void
+ {
+ if ($this->stopwatch) {
+ $this->stopwatch->lap($name);
+ }
+ }
+
+ public function isDataSchemaFileExists(string $dataSchemaFile): bool
+ {
+ try {
+ $this->dataSchemaFileLocator->locate($dataSchemaFile);
+ } catch (FileLocatorFileNotFoundException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $dataSchemaFile
+ * @return array
+ * @throws InvalidConfigurationException
+ */
+ public function getConfigurationFromFile(string $dataSchemaFile): array
+ {
+ if (isset($this->dataSchemaConfigCache[$dataSchemaFile])) {
+ return $this->dataSchemaConfigCache[$dataSchemaFile];
+ }
+
+ $dataSchemaConfig = $this->loadSchemaConfigurationFromFile($dataSchemaFile);
+
+ $dataSchemaConfig['schema'] = $dataSchemaFile;
+
+ $dataSchemaConfig = $this->processSchemaConfiguration($dataSchemaConfig);
+
+ $this->dataSchemaConfigCache[$dataSchemaFile] = $dataSchemaConfig;
+
+ return $dataSchemaConfig;
+ }
+
+ /**
+ * @param array $configuration
+ * @param string $propertyName
+ * @return array
+ * @throws InvalidConfigurationException
+ */
+ public function getPropertySourcesStack(array $configuration, string $propertyName): array
+ {
+ $depth = 0;
+ $propertiesStack = [];
+ $selects = $configuration['query']['selects'] ?? [];
+ $propertyConfig = $configuration['properties'][$propertyName] ?? null;
+
+ try {
+ while ($currentPropertyName = $propertyConfig['source'] ?? null) {
+ if (array_key_exists($currentPropertyName, $selects)) {
+ break;
+ }
+
+ if ($currentPropertyName === $propertyName) {
+ throw new InvalidConfigurationPropertyException(
+ $propertyName, "Shouldn't refer to self in \"source\" option"
+ );
+ }
+
+ $propertyConfig = $configuration['properties'][$currentPropertyName] ?? null;
+
+ if (!$propertyConfig) {
+ throw new InvalidConfigurationPropertyException(
+ $propertyName, "Invalid \"source\" option. Referred property \"$currentPropertyName\" doesn't exist in configuration."
+ );
+ }
+
+ $propertiesStack[] = [$currentPropertyName, $propertyConfig];
+
+ if (++$depth > 10) {
+ throw new InvalidConfigurationPropertyException(
+ $propertyName, "Maximum referencing depth exceeded"
+ );
+ }
+
+ }
+ } catch (InvalidConfigurationPropertyException $e) {
+ $propertiesStackString = 'Sources stack: ' . implode(
+ ' > ',
+ [$propertyName] + array_column($propertiesStack, 0)
+ );
+
+ throw new InvalidConfigurationException($configuration, $propertiesStackString . '. ' . $e->getMessage());
+ }
+
+ return $propertiesStack;
+ }
+
+ /**
+ * @param array $entityConfig
+ * @param array|null $scopeConfig
+ * @return array
+ * @throws InvalidConfigurationException
+ */
+ public function getDatabaseFields(array $entityConfig, array $scopeConfig = null): array
+ {
+ $properties = $entityConfig['properties'];
+ $entityClass = $entityConfig['class'];
+ $discriminatorMap = $entityConfig['discriminatorMap'] ?? null;
+ $databaseFields = [];
+
+ foreach ($properties as $propertyName => $propertyData) {
+ if (isset($propertyData['discriminator']) && $discriminatorMap
+ && $discriminatorMap[$propertyData['discriminator']] !== $entityClass) {
+ continue;
+ }
+ if ($scopeConfig && !array_key_exists($propertyName, $scopeConfig)) {
+ continue;
+ }
+
+ $propertySourcesStack = $this->getPropertySourcesStack($entityConfig, $propertyName);
+
+ $isVirtualProperty = !empty($propertySourcesStack);
+
+ if ($isVirtualProperty) {
+ foreach ($propertySourcesStack as [$sourcePropertyName, $sourcePropertyData]) {
+ $isValid = $sourcePropertyData['from_db'] ?? false;
+
+ if ($isValid && !in_array($sourcePropertyName, $databaseFields, true)) {
+ $databaseFields[] = $sourcePropertyName;
+ }
+ }
+ } else {
+ $isValid = $propertyData['from_db'] ?? false;
+
+ if ($isValid && !in_array($propertyName, $databaseFields, true)) {
+ $databaseFields[] = $propertyName;
+ }
+ }
+ }
+
+ return $databaseFields;
+ }
+
+ /**
+ * @param string $class
+ * @return ClassMetadata
+ */
+ public function getClassMetadata(string $class): ClassMetadata
+ {
+ return $this->doctrine->getManager()->getClassMetadata($class);
+ }
+
+ /**
+ * @throws DataTransformerNotExists
+ */
+ public function getDataTransformer(string $name): DataTransformerInterface
+ {
+ if (!$this->dataTransformerRegistry->has($name)) {
+ throw new DataTransformerNotExists($name);
+ }
+
+ return $this->dataTransformerRegistry->get($name);
+ }
+}
\ No newline at end of file
diff --git a/Service/DataSchemaValidator.php b/Service/DataSchemaValidator.php
new file mode 100644
index 0000000..a6a67f0
--- /dev/null
+++ b/Service/DataSchemaValidator.php
@@ -0,0 +1,278 @@
+dataSchemaService = $dataSchemaService;
+ }
+
+ /**
+ * @param string $dataSchemaFile
+ * @param int $nestingDepth
+ * @throws InvalidConfigurationException
+ */
+ public function validateFile(string $dataSchemaFile, int $nestingDepth = 0): void
+ {
+ $configuration = $this->dataSchemaService->getConfigurationFromFile($dataSchemaFile);
+
+ $this->validate($configuration, $nestingDepth);
+ }
+
+ /**
+ * @param array $config
+ * @param int $nestingDepth
+ * @param bool $isNested
+ * @throws InvalidConfigurationException
+ */
+ public function validate(array $config, int $nestingDepth = 0, bool $isNested = false): void
+ {
+ if ($nestingDepth < 0) {
+ throw new InvalidConfigurationException($config, "Maximum nesting depth exceeded");
+ }
+
+ try {
+ $properties = $config['properties'];
+ $class = $config['class'];
+ $schema = $config['schema'];
+
+ if ($isNested) {
+ if ($schema && !$this->dataSchemaService->isDataSchemaFileExists($schema)) {
+ throw new InvalidConfigurationException(
+ $config, "Nested property refers to nonexistent file \"$schema\""
+ );
+ }
+
+ if (!(($class && $properties) || $schema)) {
+ throw new InvalidConfigurationException(
+ $config,
+ "Nested property should have \"class\" and \"properties\" or \"schema\" property to be defined"
+ );
+ }
+ } else if (!$class || !$properties) {
+ throw new InvalidConfigurationException(
+ $config, "Should has \"class\" and \"properties\" properties to be defined and not empty"
+ );
+ }
+
+ try {
+ $classMetadata = $this->getClassMetadata($config);
+ } catch (\Exception $e) {
+ throw new InvalidConfigurationException($config, $e->getMessage());
+ }
+
+ if ($properties) {
+
+ foreach ($properties as $propertyName => $propertyConfig) {
+ $source = $propertyConfig['source'] ?? null;
+ $decode = $propertyConfig['decode'] ?? null;
+ $isNestedProperty = $propertyConfig['schema'] || $propertyConfig['properties'];
+ $isVirtualProperty = (bool)$source;
+ $hasDecodingFunction = (bool)$decode;
+
+ if ($isVirtualProperty) {
+ $this->validateVirtualProperty($config, $propertyName);
+ } else {
+ if ($classMetadata) {
+ $this->validateClassProperty(
+ $classMetadata,
+ $propertyName,
+ $propertyConfig,
+ $isNestedProperty
+ );
+ }
+
+ if ($isNestedProperty) {
+ try {
+ $this->validate($propertyConfig, $nestingDepth - 1, true);
+ } catch (InvalidConfigurationException $e) {
+ throw new InvalidConfigurationPropertyException($propertyName, $e->getMessage());
+ }
+ }
+ }
+
+ if ($hasDecodingFunction) {
+ $dataTransformerNames = $this->dataSchemaService->parseDecodeString($decode);
+
+ foreach ($dataTransformerNames as $dataTransformerName) {
+ try {
+ $this->dataSchemaService->getDataTransformer($dataTransformerName);
+ } catch (DataTransformerNotExists $e) {
+ throw new InvalidConfigurationPropertyException($propertyName, $e->getMessage());
+ }
+ }
+ }
+ }
+
+ }
+
+ } catch (InvalidConfigurationPropertyException | InvalidConfigurationException $e) {
+ throw new InvalidConfigurationException($config, $e->getMessage());
+ }
+ }
+
+ /**
+ * @param ClassMetadata $classMetadata
+ * @param string $name
+ * @param array $config
+ * @param bool $isNested
+ * @return void
+ * @throws InvalidConfigurationPropertyException
+ */
+ private function validateClassProperty(ClassMetadata $classMetadata,
+ string $name,
+ array $config,
+ bool $isNested): void
+ {
+ $class = $classMetadata->getName();
+ $discriminator = $config['discriminator'] ?? null;
+
+ if (!$classMetadata->hasField($name) && !$classMetadata->hasAssociation($name)) {
+ $discriminatorMap = $classMetadata->discriminatorMap;
+ if (!$discriminatorMap) {
+ $properties = $this->getAvailableProperties($classMetadata);
+
+ throw new InvalidConfigurationPropertyException(
+ $name, "Not found in class \"$class\". Available properties: " . json_encode($properties)
+ );
+ }
+
+ if ($discriminator) {
+ $subClass = $discriminatorMap[$discriminator] ?? null;
+
+ if ($subClass) {
+ $subClassMetadata = $this->dataSchemaService->getClassMetadata($subClass);
+ if ($isNested && !$subClassMetadata->hasAssociation($name)) {
+ throw new InvalidConfigurationPropertyException(
+ $name, "Nested property should have association mapping"
+ );
+ }
+
+ if (!$subClassMetadata->hasField($name) && !$subClassMetadata->hasAssociation($name)) {
+ $this->findPropertyAndThrowExceptionIfFound($subClass, $name, $discriminatorMap);
+
+ throw new InvalidConfigurationPropertyException(
+ $name, "Class \"$subClass\" and all its siblings doesn't have this property"
+ );
+ }
+ } else {
+ $discriminators = array_keys($discriminatorMap);
+ throw new InvalidConfigurationPropertyException(
+ $name, "Invalid discriminator \"$discriminator\". Available discriminators: " . json_encode(
+ $discriminators
+ )
+ );
+ }
+ } else {
+ if ($isNested && !$classMetadata->hasAssociation($name)) {
+ throw new InvalidConfigurationPropertyException(
+ $name, "Nested property should have association mapping"
+ );
+ }
+
+ $this->findPropertyAndThrowExceptionIfFound($class, $name, $discriminatorMap);
+
+ throw new InvalidConfigurationPropertyException(
+ $name, "Class \"$class\" and all its subclasses doesn't have this property"
+ );
+ }
+ } else {
+// @TODO fix in glavweb-datagrid-bundle
+// if ($discriminator) {
+// throw new InvalidConfigurationPropertyException(
+// $name, "Shouldn't have \"discriminator\" property defined"
+// );
+// }
+ }
+
+ }
+
+ /**
+ * @param array $config
+ * @param $name
+ * @return void
+ * @throws InvalidConfigurationException
+ */
+ private function validateVirtualProperty(array $config, $name): void
+ {
+ $this->dataSchemaService->getPropertySourcesStack($config, $name);
+ }
+
+ /**
+ * @param $class
+ * @param string $name
+ * @param array $discriminatorMap
+ * @throws InvalidConfigurationPropertyException
+ */
+ private function findPropertyAndThrowExceptionIfFound($class, string $name, array $discriminatorMap): void
+ {
+ foreach ($discriminatorMap as $discriminator => $mappedClass) {
+ if ($class === $mappedClass) {
+ continue;
+ }
+
+ $mappedClassMetadata = $this->dataSchemaService->getClassMetadata($mappedClass);
+
+ if ($mappedClassMetadata->hasField($name) || $mappedClassMetadata->hasAssociation($name)) {
+ throw new InvalidConfigurationPropertyException(
+ $name,
+ "Class \"$class\" don't have this property, but \"$mappedClass\" has. "
+ . "You probably meant to use the \"$discriminator\" discriminator"
+ );
+ }
+ }
+ }
+
+ /**
+ * @param array $config
+ * @return ClassMetadata|null
+ */
+ private function getClassMetadata(array $config): ?ClassMetadata
+ {
+ $class = $config['class'] ?? null;
+
+ return $class ? $this->dataSchemaService->getClassMetadata($class) : null;
+ }
+
+ /**
+ * @param ClassMetadata $classMetadata
+ * @return string[]
+ */
+ private function getAvailableProperties(ClassMetadata $classMetadata): array
+ {
+ $allProperties = array_merge($classMetadata->getFieldNames(), $classMetadata->getAssociationNames());
+
+ return array_map(
+ static function ($name) use ($classMetadata) {
+ if ($classMetadata->hasAssociation($name)) {
+ $type = $classMetadata->getAssociationTargetClass($name);
+ } else {
+ $type = $classMetadata->getTypeOfField($name);
+ }
+
+ if ($classMetadata->isCollectionValuedAssociation($name)) {
+ $type .= '[]';
+ }
+
+ return "$name: $type";
+ },
+ $allProperties
+ );
+ }
+}
\ No newline at end of file
diff --git a/Util/Utils.php b/Util/Utils.php
new file mode 100755
index 0000000..bdfb610
--- /dev/null
+++ b/Util/Utils.php
@@ -0,0 +1,34 @@
+
+ */
+class Utils
+{
+ public static function arrayDeepMerge(...$arrays): array
+ {
+ $result = [];
+
+ foreach ($arrays as $array) {
+ foreach ($array as $key => $value) {
+ if (is_int($key)) {
+ $result[] = $value;
+
+ } elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
+ $result[$key] = self::arrayDeepMerge($result[$key], $value);
+
+ } else {
+ $result[$key] = $value;
+ }
+ }
+ }
+
+ return $result;
+ }
+}
\ No newline at end of file
diff --git a/composer.json b/composer.json
old mode 100644
new mode 100755
index c5bb4bf..c527768
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
"require": {
"php": "^7.1.3",
"symfony/config": "^2.7|^3.0|^4.0",
+ "symfony/finder": "^2.7|^3.0|^4.0",
"symfony/dependency-injection": "^2.7|^3.0|^4.0",
"symfony/yaml": "^2.7|^3.0|^4.0",
"doctrine/orm": "^2.3",