diff --git a/UPGRADE-1.10.3.md b/UPGRADE-1.10.3.md index fb753a2a9f1..2d6b71e4704 100644 --- a/UPGRADE-1.10.3.md +++ b/UPGRADE-1.10.3.md @@ -55,3 +55,15 @@ oro_email.email_address.entity_manager: ####LocaleBundle - `oro_locale.repository.localization` inherits `oro_entity.abstract_repository` + +####DatagridBundle: +- Class `Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource.php` + - construction signature was changed now it takes next arguments: + `ConfigProcessorInterface` $processor, + `EventDispatcherInterface` $eventDispatcher, + `ParameterBinderInterface` $parameterBinder, + `QueryHintResolver` $queryHintResolver +- Added class `Oro\Bundle\DataGridBundle\Datasource\Orm\Configs\YamlProcessor` +- Added interface `Oro\Bundle\DataGridBundle\Datasource\Orm\Configs\ConfigProcessorInterface` +- `Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource::getParameterBinder` was deprecated +- `Oro\Bundle\DataGridBundle\Datasource\ParameterBinderAwareInterface::getParameterBinder` was deprecated diff --git a/composer.json b/composer.json index d5c7cef31fc..b5d56d4bd85 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "composer/composer": "1.2.*", "akeneo/batch-bundle": "0.4.2", "nesbot/Carbon": "1.8.*", - "monolog/monolog": "1.8.*", + "monolog/monolog": "^1.17", "ocramius/proxy-manager": "~0.4", "knplabs/knp-gaufrette-bundle": "0.1.*", "oro/doctrine-extensions": "1.0.*", diff --git a/src/Oro/Bundle/ApiBundle/ApiDoc/RestDocHandler.php b/src/Oro/Bundle/ApiBundle/ApiDoc/RestDocHandler.php index 337a6462e76..67c606ac420 100644 --- a/src/Oro/Bundle/ApiBundle/ApiDoc/RestDocHandler.php +++ b/src/Oro/Bundle/ApiBundle/ApiDoc/RestDocHandler.php @@ -11,7 +11,7 @@ use Oro\Bundle\ApiBundle\Config\DescriptionsConfigExtra; use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Config\StatusCodesConfig; -use Oro\Bundle\ApiBundle\Filter\ComparisonFilter; +use Oro\Bundle\ApiBundle\Filter\FieldAwareFilterInterface; use Oro\Bundle\ApiBundle\Filter\FilterCollection; use Oro\Bundle\ApiBundle\Filter\StandaloneFilter; use Oro\Bundle\ApiBundle\Filter\StandaloneFilterWithDefaultValue; @@ -294,18 +294,20 @@ protected function addFilters(ApiDoc $annotation, FilterCollection $filters, Ent } } - if ($filter instanceof ComparisonFilter && $metadata->hasAssociation($filter->getField())) { - $targetClassNames = $metadata->getAssociation($filter->getField()) - ->getAcceptableTargetClassNames(); - $targetEntityTypes = []; - foreach ($targetClassNames as $targetClassName) { - $targetEntityType = $this->getEntityType($targetClassName); - if ($targetEntityType) { - $targetEntityTypes[] = $targetEntityType; + if ($filter instanceof FieldAwareFilterInterface) { + $association = $metadata->getAssociation($filter->getField()); + if (null !== $association && !DataType::isAssociationAsField($association->getDataType())) { + $targetClassNames = $association->getAcceptableTargetClassNames(); + $targetEntityTypes = []; + foreach ($targetClassNames as $targetClassName) { + $targetEntityType = $this->getEntityType($targetClassName); + if ($targetEntityType) { + $targetEntityTypes[] = $targetEntityType; + } + } + if (!empty($targetEntityTypes)) { + $options['relation'] = implode(',', $targetEntityTypes); } - } - if (!empty($targetEntityTypes)) { - $options['relation'] = implode(',', $targetEntityTypes); } } diff --git a/src/Oro/Bundle/ApiBundle/Command/AbstractDebugCommand.php b/src/Oro/Bundle/ApiBundle/Command/AbstractDebugCommand.php index e3b7f1602f7..415b1f5b763 100644 --- a/src/Oro/Bundle/ApiBundle/Command/AbstractDebugCommand.php +++ b/src/Oro/Bundle/ApiBundle/Command/AbstractDebugCommand.php @@ -120,9 +120,9 @@ protected function resolveEntityClass($entityName, $version, RequestType $reques /** @var ResourcesProvider $resourcesProvider */ $resourcesProvider = $this->getContainer()->get('oro_api.resources_provider'); - if (!$resourcesProvider->isResourceAccessible($entityClass, $version, $requestType)) { + if (!$resourcesProvider->isResourceKnown($entityClass, $version, $requestType)) { throw new \RuntimeException( - sprintf('The "%s" entity is not accessible through Data API.', $entityClass) + sprintf('The "%s" entity is not configured to be used in Data API.', $entityClass) ); } diff --git a/src/Oro/Bundle/ApiBundle/Command/DumpCommand.php b/src/Oro/Bundle/ApiBundle/Command/DumpCommand.php index 80a738bc38d..c75d4477ebd 100644 --- a/src/Oro/Bundle/ApiBundle/Command/DumpCommand.php +++ b/src/Oro/Bundle/ApiBundle/Command/DumpCommand.php @@ -19,6 +19,7 @@ use Oro\Bundle\ApiBundle\Request\RequestType; use Oro\Bundle\ApiBundle\Request\ValueNormalizer; use Oro\Bundle\ApiBundle\Request\Version; +use Oro\Bundle\ApiBundle\Util\ValueNormalizerUtil; class DumpCommand extends AbstractDebugCommand { @@ -154,7 +155,8 @@ public function dumpResources(InputInterface $input, OutputInterface $output) ); if ($isSubresourcesRequested) { $subresourcesText = $this->getEntitySubresourcesText( - $subresourcesProvider->getSubresources($resource->getEntityClass(), $version, $requestType) + $subresourcesProvider->getSubresources($resource->getEntityClass(), $version, $requestType), + $requestType ); if ($subresourcesText) { $output->writeln($subresourcesText); @@ -165,17 +167,28 @@ public function dumpResources(InputInterface $input, OutputInterface $output) /** * @param ApiResourceSubresources $entitySubresources + * @param RequestType $requestType * * @return string */ - protected function getEntitySubresourcesText(ApiResourceSubresources $entitySubresources) - { + protected function getEntitySubresourcesText( + ApiResourceSubresources $entitySubresources, + $requestType + ) { $result = ''; $subresources = $entitySubresources->getSubresources(); if (!empty($subresources)) { $result .= ' Sub-resources:'; foreach ($subresources as $associationName => $subresource) { + $targetEntityType = $this->resolveEntityType($subresource->getTargetClassName(), $requestType); + $acceptableTargetEntityTypes = []; + foreach ($subresource->getAcceptableTargetClassNames() as $className) { + $acceptableTargetEntityTypes[] = $this->resolveEntityType($className, $requestType); + } $result .= sprintf("\n %s", $associationName); + $result .= "\n Type: " . ($subresource->isCollection() ? 'to-many' : 'to-one'); + $result .= "\n Target: " . $targetEntityType; + $result .= "\n Acceptable Targets: " . implode(', ', $acceptableTargetEntityTypes); $subresourceExcludedActions = $subresource->getExcludedActions(); if (!empty($subresourceExcludedActions)) { $result .= "\n Excluded Actions: " . implode(', ', $subresourceExcludedActions); @@ -234,4 +247,23 @@ protected function convertResourceAttributesToString(array $attributes) return $result; } + + /** + * @param string|null $entityClass + * @param RequestType $requestType + * + * @return string|null + */ + protected function resolveEntityType($entityClass, RequestType $requestType) + { + if (!$entityClass) { + return null; + } + + return ValueNormalizerUtil::convertToEntityType( + $this->getContainer()->get('oro_api.value_normalizer'), + $entityClass, + $requestType + ); + } } diff --git a/src/Oro/Bundle/ApiBundle/Command/DumpConfigCommand.php b/src/Oro/Bundle/ApiBundle/Command/DumpConfigCommand.php index 8c079f0865a..d0ec55f1cb3 100644 --- a/src/Oro/Bundle/ApiBundle/Command/DumpConfigCommand.php +++ b/src/Oro/Bundle/ApiBundle/Command/DumpConfigCommand.php @@ -186,11 +186,7 @@ protected function getConfig($entityClass, $version, RequestType $requestType, a $config = $configProvider->getConfig($entityClass, $version, $requestType, $extras); return [ - 'oro_api' => [ - 'entities' => [ - $entityClass => $this->convertConfigToArray($config) - ] - ] + $entityClass => $this->convertConfigToArray($config) ]; } @@ -210,11 +206,7 @@ protected function getRelationConfig($entityClass, $version, RequestType $reques $config = $configProvider->getRelationConfig($entityClass, $version, $requestType, $extras); return [ - 'oro_api' => [ - 'relations' => [ - $entityClass => $this->convertConfigToArray($config) - ] - ] + $entityClass => $this->convertConfigToArray($config) ]; } diff --git a/src/Oro/Bundle/ApiBundle/Command/DumpMetadataCommand.php b/src/Oro/Bundle/ApiBundle/Command/DumpMetadataCommand.php index bf8c5e622bf..6d3276f5976 100644 --- a/src/Oro/Bundle/ApiBundle/Command/DumpMetadataCommand.php +++ b/src/Oro/Bundle/ApiBundle/Command/DumpMetadataCommand.php @@ -98,11 +98,7 @@ protected function getMetadata($entityClass, $version, RequestType $requestType, ); return [ - 'oro_api' => [ - 'metadata' => [ - $entityClass => null !== $metadata ? $metadata->toArray() : null - ] - ] + $entityClass => null !== $metadata ? $metadata->toArray() : null ]; } } diff --git a/src/Oro/Bundle/ApiBundle/Config/ConfigAccessorInterface.php b/src/Oro/Bundle/ApiBundle/Config/ConfigAccessorInterface.php new file mode 100644 index 00000000000..5eed44ac43c --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Config/ConfigAccessorInterface.php @@ -0,0 +1,15 @@ +end() ->useAttributeAsKey('name') + ->beforeNormalization() + ->always(function ($value) { + return false === $value + ? array_fill_keys($this->permissibleActions, false) + : $value; + }) + ->end() ->validate() ->always(function ($value) { $unknownActions = array_diff(array_keys($value), $this->permissibleActions); diff --git a/src/Oro/Bundle/ApiBundle/Config/Definition/FiltersConfiguration.php b/src/Oro/Bundle/ApiBundle/Config/Definition/FiltersConfiguration.php index 0b91fcb1aa5..da7211a3379 100644 --- a/src/Oro/Bundle/ApiBundle/Config/Definition/FiltersConfiguration.php +++ b/src/Oro/Bundle/ApiBundle/Config/Definition/FiltersConfiguration.php @@ -58,13 +58,45 @@ protected function configureFieldNode(NodeBuilder $node) //$parentNode->ignoreExtraKeys(false); @todo: uncomment after migration to Symfony 2.8+ $this->callConfigureCallbacks($node, $sectionName); $this->addPreProcessCallbacks($parentNode, $sectionName); - $this->addPostProcessCallbacks($parentNode, $sectionName); + $this->addPostProcessCallbacks( + $parentNode, + $sectionName, + function ($value) { + return $this->postProcessFieldConfig($value); + } + ); $node ->booleanNode(FilterFieldConfig::EXCLUDE)->end() ->scalarNode(FilterFieldConfig::DESCRIPTION)->cannotBeEmpty()->end() ->scalarNode(FilterFieldConfig::PROPERTY_PATH)->cannotBeEmpty()->end() + ->scalarNode(FilterFieldConfig::TYPE)->cannotBeEmpty()->end() + ->arrayNode(FilterFieldConfig::OPTIONS) + ->useAttributeAsKey('name') + ->performNoDeepMerging() + ->prototype('variable')->end() + ->end() + ->arrayNode(FilterFieldConfig::OPERATORS) + ->prototype('scalar')->end() + ->end() ->scalarNode(FilterFieldConfig::DATA_TYPE)->cannotBeEmpty()->end() ->booleanNode(FilterFieldConfig::ALLOW_ARRAY)->end(); } + + /** + * @param array $config + * + * @return array + */ + protected function postProcessFieldConfig(array $config) + { + if (empty($config[FilterFieldConfig::OPTIONS])) { + unset($config[FilterFieldConfig::OPTIONS]); + } + if (empty($config[FilterFieldConfig::OPERATORS])) { + unset($config[FilterFieldConfig::OPERATORS]); + } + + return $config; + } } diff --git a/src/Oro/Bundle/ApiBundle/Config/FieldConfigInterface.php b/src/Oro/Bundle/ApiBundle/Config/FieldConfigInterface.php index 404b0b1c16d..428bcf5d752 100644 --- a/src/Oro/Bundle/ApiBundle/Config/FieldConfigInterface.php +++ b/src/Oro/Bundle/ApiBundle/Config/FieldConfigInterface.php @@ -68,9 +68,11 @@ public function hasPropertyPath(); /** * Gets the path of the field value. * + * @param string|null $defaultValue + * * @return string|null */ - public function getPropertyPath(); + public function getPropertyPath($defaultValue = null); /** * Sets the path of the field value. diff --git a/src/Oro/Bundle/ApiBundle/Config/FilterFieldConfig.php b/src/Oro/Bundle/ApiBundle/Config/FilterFieldConfig.php index 293657b0466..543c270dc77 100644 --- a/src/Oro/Bundle/ApiBundle/Config/FilterFieldConfig.php +++ b/src/Oro/Bundle/ApiBundle/Config/FilterFieldConfig.php @@ -23,6 +23,15 @@ class FilterFieldConfig implements FieldConfigInterface /** the path of the field value */ const PROPERTY_PATH = EntityDefinitionFieldConfig::PROPERTY_PATH; + /** the type of the filter */ + const TYPE = 'type'; + + /** the filter options */ + const OPTIONS = 'options'; + + /** a list of operators supported by the filter */ + const OPERATORS = 'operators'; + /** the data type of the filter value */ const DATA_TYPE = EntityDefinitionFieldConfig::DATA_TYPE; @@ -54,6 +63,84 @@ public function __clone() $this->items = ConfigUtil::cloneItems($this->items); } + /** + * Gets the filter type. + * + * @return string|null + */ + public function getType() + { + return array_key_exists(self::TYPE, $this->items) + ? $this->items[self::TYPE] + : null; + } + + /** + * Sets the filter type. + * + * @param string|null $type + */ + public function setType($type) + { + if ($type) { + $this->items[self::TYPE] = $type; + } else { + unset($this->items[self::TYPE]); + } + } + + /** + * Gets the filter options. + * + * @return array|null + */ + public function getOptions() + { + return array_key_exists(self::OPTIONS, $this->items) + ? $this->items[self::OPTIONS] + : null; + } + + /** + * Sets the filter options. + * + * @param array|null $options + */ + public function setOptions($options) + { + if ($options) { + $this->items[self::OPTIONS] = $options; + } else { + unset($this->items[self::OPTIONS]); + } + } + + /** + * Gets a list of operators supported by the filter. + * + * @return string[]|null + */ + public function getOperators() + { + return array_key_exists(self::OPERATORS, $this->items) + ? $this->items[self::OPERATORS] + : null; + } + + /** + * Sets a list of operators supported by the filter. + * + * @param string[]|null $operators + */ + public function setOperators($operators) + { + if ($operators) { + $this->items[self::OPERATORS] = $operators; + } else { + unset($this->items[self::OPERATORS]); + } + } + /** * Indicates whether the "array allowed" flag is set explicitly. * diff --git a/src/Oro/Bundle/ApiBundle/Config/Traits/FieldConfigTrait.php b/src/Oro/Bundle/ApiBundle/Config/Traits/FieldConfigTrait.php index 1619171865c..0fc66f1c37f 100644 --- a/src/Oro/Bundle/ApiBundle/Config/Traits/FieldConfigTrait.php +++ b/src/Oro/Bundle/ApiBundle/Config/Traits/FieldConfigTrait.php @@ -24,13 +24,15 @@ public function hasPropertyPath() /** * Gets the path of the field value. * + * @param string|null $defaultValue + * * @return string|null */ - public function getPropertyPath() + public function getPropertyPath($defaultValue = null) { - return array_key_exists(FieldConfig::PROPERTY_PATH, $this->items) + return !empty($this->items[FieldConfig::PROPERTY_PATH]) ? $this->items[FieldConfig::PROPERTY_PATH] - : null; + : $defaultValue; } /** diff --git a/src/Oro/Bundle/ApiBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/ApiBundle/DependencyInjection/Configuration.php index 1963a1a48aa..4883b1b69dd 100644 --- a/src/Oro/Bundle/ApiBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/ApiBundle/DependencyInjection/Configuration.php @@ -17,6 +17,7 @@ public function getConfigTreeBuilder() $rootNode = $treeBuilder->root('oro_api'); $node = $rootNode->children(); + $this->appendConfigOptions($node); $this->appendActionsNode($node); $this->appendFiltersNode($node); $this->appendFormTypesNode($node); @@ -27,6 +28,27 @@ public function getConfigTreeBuilder() return $treeBuilder; } + /** + * @param NodeBuilder $node + */ + protected function appendConfigOptions(NodeBuilder $node) + { + $node + ->integerNode('config_max_nesting_level') + ->info( + 'The maximum number of nesting target entities' + . ' that can be specified in "Resources/config/oro/api.yml"' + ) + ->min(0) + ->defaultValue(3) + ->end() + ->arrayNode('api_doc_views') + ->info('All supported ApiDoc views') + ->prototype('scalar')->end() + ->defaultValue(['default']) + ->end(); + } + /** * @param NodeBuilder $node */ diff --git a/src/Oro/Bundle/ApiBundle/DependencyInjection/OroApiExtension.php b/src/Oro/Bundle/ApiBundle/DependencyInjection/OroApiExtension.php index 831a56703cd..24afefcf838 100644 --- a/src/Oro/Bundle/ApiBundle/DependencyInjection/OroApiExtension.php +++ b/src/Oro/Bundle/ApiBundle/DependencyInjection/OroApiExtension.php @@ -19,6 +19,8 @@ class OroApiExtension extends Extension implements PrependExtensionInterface { + const API_DOC_VIEWS_PARAMETER_NAME = 'oro_api.api_doc.views'; + const ACTION_PROCESSOR_BAG_SERVICE_ID = 'oro_api.action_processor_bag'; const ACTION_PROCESSOR_TAG = 'oro.api.action_processor'; const CONFIG_EXTENSION_REGISTRY_SERVICE_ID = 'oro_api.config_extension_registry'; @@ -29,6 +31,7 @@ class OroApiExtension extends Extension implements PrependExtensionInterface const COLLECT_RESOURCES_PROCESSOR_SERVICE_ID = 'oro_api.collect_resources.processor'; const COLLECT_SUBRESOURCES_PROCESSOR_SERVICE_ID = 'oro_api.collect_subresources.processor'; const CUSTOMIZE_LOADED_DATA_PROCESSOR_SERVICE_ID = 'oro_api.customize_loaded_data.processor'; + const CUSTOMIZE_FORM_DATA_PROCESSOR_SERVICE_ID = 'oro_api.customize_form_data.processor'; const GET_CONFIG_PROCESSOR_SERVICE_ID = 'oro_api.get_config.processor'; const GET_RELATION_CONFIG_PROCESSOR_SERVICE_ID = 'oro_api.get_relation_config.processor'; const GET_METADATA_PROCESSOR_SERVICE_ID = 'oro_api.get_metadata.processor'; @@ -43,6 +46,8 @@ class OroApiExtension extends Extension implements PrependExtensionInterface */ public function load(array $configs, ContainerBuilder $container) { + $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yml'); $loader->load('data_transformers.yml'); @@ -73,6 +78,7 @@ public function load(array $configs, ContainerBuilder $container) * To load configuration we need fully configured config tree builder, * that's why the action processors bag and all configuration extensions should be registered before. */ + $this->registerConfigParameters($container, $config); $this->registerActionProcessors($container); $this->registerConfigExtensions($container); @@ -150,6 +156,10 @@ protected function registerDebugServices(ContainerBuilder $container) $container, self::CUSTOMIZE_LOADED_DATA_PROCESSOR_SERVICE_ID ); + DependencyInjectionUtil::registerDebugService( + $container, + self::CUSTOMIZE_FORM_DATA_PROCESSOR_SERVICE_ID + ); DependencyInjectionUtil::registerDebugService( $container, self::GET_CONFIG_PROCESSOR_SERVICE_ID @@ -209,6 +219,18 @@ protected function loadApiConfiguration(ContainerBuilder $container) $chainProviderDef->replaceArgument(1, $inclusions); } + /** + * @param ContainerBuilder $container + * @param array $config + */ + protected function registerConfigParameters(ContainerBuilder $container, array $config) + { + $container + ->getDefinition(self::CONFIG_EXTENSION_REGISTRY_SERVICE_ID) + ->replaceArgument(0, $config['config_max_nesting_level']); + $container->setParameter(self::API_DOC_VIEWS_PARAMETER_NAME, $config['api_doc_views']); + } + /** * @param ContainerBuilder $container */ diff --git a/src/Oro/Bundle/ApiBundle/Filter/ChainFilterFactory.php b/src/Oro/Bundle/ApiBundle/Filter/ChainFilterFactory.php index 0ea4a07de67..98ce6262276 100644 --- a/src/Oro/Bundle/ApiBundle/Filter/ChainFilterFactory.php +++ b/src/Oro/Bundle/ApiBundle/Filter/ChainFilterFactory.php @@ -20,10 +20,10 @@ public function addFilterFactory(FilterFactoryInterface $filterFactory) /** * {@inheritdoc} */ - public function createFilter($dataType) + public function createFilter($filterType, array $options = []) { foreach ($this->factories as $factory) { - $filter = $factory->createFilter($dataType); + $filter = $factory->createFilter($filterType, $options); if (null !== $filter) { return $filter; } diff --git a/src/Oro/Bundle/ApiBundle/Filter/ComparisonFilter.php b/src/Oro/Bundle/ApiBundle/Filter/ComparisonFilter.php index 29a1777d756..f8d0a862089 100644 --- a/src/Oro/Bundle/ApiBundle/Filter/ComparisonFilter.php +++ b/src/Oro/Bundle/ApiBundle/Filter/ComparisonFilter.php @@ -10,7 +10,7 @@ * Also this filter supports different kind of comparison: * "equal", "not equal", "less than", "less than or equal", "greater than", "greater than or equal". */ -class ComparisonFilter extends StandaloneFilter +class ComparisonFilter extends StandaloneFilter implements FieldAwareFilterInterface { const NEQ = '!='; const LT = '<'; @@ -22,7 +22,7 @@ class ComparisonFilter extends StandaloneFilter protected $field; /** - * Gets a field by which data should be filtered + * Gets a field by which the data is filtered. * * @return string|null */ @@ -32,9 +32,7 @@ public function getField() } /** - * Sets a field by which data should be filtered - * - * @param string $field + * {@inheritdoc} */ public function setField($field) { @@ -63,9 +61,13 @@ public function apply(Criteria $criteria, FilterValue $value = null) } /** - * {@inheritdoc} + * Creates an expression that can be used to in WHERE statement to filter data by this filter. + * + * @param FilterValue|null $value + * + * @return Expression|null */ - public function createExpression(FilterValue $value = null) + protected function createExpression(FilterValue $value = null) { return null !== $value ? $this->buildExpression($this->field, $value->getOperator(), $value->getValue()) @@ -86,7 +88,7 @@ public function createExpression(FilterValue $value = null) protected function buildExpression($field, $operator, $value) { if (!$field) { - throw new \InvalidArgumentException('Field must not be empty.'); + throw new \InvalidArgumentException('The Field must not be empty.'); } if (null === $value) { throw new \InvalidArgumentException( diff --git a/src/Oro/Bundle/ApiBundle/Filter/FieldAwareFilterInterface.php b/src/Oro/Bundle/ApiBundle/Filter/FieldAwareFilterInterface.php new file mode 100644 index 00000000000..c946eaeafbd --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Filter/FieldAwareFilterInterface.php @@ -0,0 +1,16 @@ +filterMap = []; foreach ($this->filters as $filterKey => $filter) { - if ($filter instanceof ComparisonFilter) { + if ($filter instanceof FieldAwareFilterInterface) { $this->filterMap[$filter->getField()] = [ $this->filterValues->get($filterKey), $filterKey, diff --git a/src/Oro/Bundle/ApiBundle/Filter/FilterInterface.php b/src/Oro/Bundle/ApiBundle/Filter/FilterInterface.php index 625c574f5c2..40a861fa4d0 100644 --- a/src/Oro/Bundle/ApiBundle/Filter/FilterInterface.php +++ b/src/Oro/Bundle/ApiBundle/Filter/FilterInterface.php @@ -3,7 +3,6 @@ namespace Oro\Bundle\ApiBundle\Filter; use Doctrine\Common\Collections\Criteria; -use Doctrine\Common\Collections\Expr\Expression; /** * Provides an interface for different kind of data filters. @@ -17,13 +16,4 @@ interface FilterInterface * @param FilterValue|null $value */ public function apply(Criteria $criteria, FilterValue $value = null); - - /** - * Creates an expression that can be used to in WHERE statement to filter data by this filter. - * - * @param FilterValue|null $value - * - * @return Expression|null - */ - public function createExpression(FilterValue $value = null); } diff --git a/src/Oro/Bundle/ApiBundle/Filter/PrimaryFieldFilter.php b/src/Oro/Bundle/ApiBundle/Filter/PrimaryFieldFilter.php new file mode 100644 index 00000000000..0c49e988731 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Filter/PrimaryFieldFilter.php @@ -0,0 +1,85 @@ +dataField; + } + + /** + * Sets a field that contains a data value. + * + * @param string $fieldName + */ + public function setDataField($fieldName) + { + $this->dataField = $fieldName; + } + + /** + * Gets a field that contains a "primary" flag. + * + * @return string|null + */ + public function getPrimaryFlagField() + { + return $this->primaryFlagField; + } + + /** + * Sets a field that contains a "primary" flag. + * + * @param string $fieldName + */ + public function setPrimaryFlagField($fieldName) + { + $this->primaryFlagField = $fieldName; + } + + /** + * {@inheritdoc} + */ + protected function createExpression(FilterValue $value = null) + { + if (null === $value) { + return null; + } + + if (!$this->field) { + throw new \InvalidArgumentException('The Field must not be empty.'); + } + if (!$this->dataField) { + throw new \InvalidArgumentException('The DataField must not be empty.'); + } + + $expr = $this->buildExpression( + $this->field . '.' . $this->dataField, + $value->getOperator(), + $value->getValue() + ); + + return Criteria::expr()->andX( + $expr, + Criteria::expr()->eq($this->field . '.' . ($this->primaryFlagField ?: 'primary'), true) + ); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Filter/SimpleFilterFactory.php b/src/Oro/Bundle/ApiBundle/Filter/SimpleFilterFactory.php index f93d4349d34..234e65bc97f 100644 --- a/src/Oro/Bundle/ApiBundle/Filter/SimpleFilterFactory.php +++ b/src/Oro/Bundle/ApiBundle/Filter/SimpleFilterFactory.php @@ -23,31 +23,34 @@ public function __construct(PropertyAccessorInterface $propertyAccessor) /** * Registers a filter that should be used to handle the given data-type. * - * @param string $dataType The data-type of a value. + * @param string $filterType The type of a filter. * @param string $filterClassName The class name of a filter. Should extents StandaloneFilter. * @param array $parameters Additional parameters for the filter. [property name => value, ...] */ - public function addFilter($dataType, $filterClassName, array $parameters = []) + public function addFilter($filterType, $filterClassName, array $parameters = []) { - $this->filters[$dataType] = [$filterClassName, $parameters]; + $this->filters[$filterType] = [$filterClassName, $parameters]; } /** * {@inheritdoc} */ - public function createFilter($dataType) + public function createFilter($filterType, array $options = []) { - if (!isset($this->filters[$dataType])) { + if (!isset($this->filters[$filterType])) { return null; } - $options = $this->filters[$dataType]; - $filterClassName = $options[0]; + list($filterClassName, $parameters) = $this->filters[$filterType]; + $options = array_replace($parameters, $options); + $dataType = $filterType; + if (array_key_exists(self::DATA_TYPE_OPTION, $options)) { + $dataType = $options[self::DATA_TYPE_OPTION]; + unset($options[self::DATA_TYPE_OPTION]); + } $filter = new $filterClassName($dataType); - if (!empty($options[1])) { - foreach ($options[1] as $name => $value) { - $this->propertyAccessor->setValue($filter, $name, $value); - } + foreach ($options as $name => $value) { + $this->propertyAccessor->setValue($filter, $name, $value); } return $filter; diff --git a/src/Oro/Bundle/ApiBundle/Filter/StandaloneFilter.php b/src/Oro/Bundle/ApiBundle/Filter/StandaloneFilter.php index c810f22d994..3b4fe688eff 100644 --- a/src/Oro/Bundle/ApiBundle/Filter/StandaloneFilter.php +++ b/src/Oro/Bundle/ApiBundle/Filter/StandaloneFilter.php @@ -123,12 +123,4 @@ public function setSupportedOperators(array $operators) public function apply(Criteria $criteria, FilterValue $value = null) { } - - /** - * {@inheritdoc} - */ - public function createExpression(FilterValue $value = null) - { - return null; - } } diff --git a/src/Oro/Bundle/ApiBundle/Form/EventListener/CollectionEntryFactory.php b/src/Oro/Bundle/ApiBundle/Form/EventListener/CollectionEntryFactory.php new file mode 100644 index 00000000000..6d49c1258cc --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/EventListener/CollectionEntryFactory.php @@ -0,0 +1,58 @@ +dataClass = $dataClass; + $this->type = $type; + $this->options = $options; + } + + /** + * Creates a form for the collection entry. + * + * @param FormFactoryInterface $factory The form factory + * @param string $name The collection entry field name + * + * @return FormInterface + */ + public function createEntry(FormFactoryInterface $factory, $name) + { + return $factory->createNamed( + $name, + $this->type, + null, + array_replace( + [ + 'auto_initialize' => false, + 'data_class' => $this->dataClass, + 'property_path' => '[' . $name . ']', + 'error_bubbling' => false, + 'constraints' => new Assert\Valid() + ], + $this->options + ) + ); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/EventListener/CollectionListener.php b/src/Oro/Bundle/ApiBundle/Form/EventListener/CollectionListener.php new file mode 100644 index 00000000000..a568068959f --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/EventListener/CollectionListener.php @@ -0,0 +1,139 @@ +entryFactory = $entryFactory; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + FormEvents::PRE_SET_DATA => 'preSetData', + FormEvents::PRE_SUBMIT => 'preSubmit', + // (MergeCollectionListener, MergeDoctrineCollectionListener) + FormEvents::SUBMIT => ['onSubmit', 50] + ]; + } + + /** + * @param FormEvent $event + */ + public function preSetData(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData(); + + if (null === $data) { + $data = []; + } + + if (!$this->isSupportedData($data)) { + throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); + } + + // First remove all rows + foreach ($form as $name => $child) { + $form->remove($name); + } + + // Then add all rows again in the correct order + $factory = $form->getConfig()->getFormFactory(); + foreach ($data as $name => $value) { + $form->add($this->entryFactory->createEntry($factory, $name)); + } + } + + /** + * @param FormEvent $event + */ + public function preSubmit(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData(); + + if (!$this->isSupportedData($data)) { + $data = []; + } + + // Remove all empty rows + foreach ($form as $name => $child) { + if (!isset($data[$name])) { + $form->remove($name); + } + } + + // Add all additional rows + $factory = $form->getConfig()->getFormFactory(); + foreach ($data as $name => $value) { + if (!$form->has($name)) { + $form->add($this->entryFactory->createEntry($factory, $name)); + } + } + } + + /** + * @param FormEvent $event + */ + public function onSubmit(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData(); + + // At this point, $data is an array or an array-like object that already contains the + // new entries, which were added by the data mapper. The data mapper ignores existing + // entries, so we need to manually unset removed entries in the collection. + + if (null === $data) { + $data = []; + } + + if (!$this->isSupportedData($data)) { + throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); + } + + // The data mapper only adds, but does not remove items, so do this here + $toDelete = []; + foreach ($data as $name => $child) { + if (!$form->has($name)) { + $toDelete[] = $name; + } + } + foreach ($toDelete as $name) { + unset($data[$name]); + } + + $event->setData($data); + } + + /** + * @param mixed $data + * + * @return bool + */ + protected function isSupportedData($data) + { + return is_array($data) || ($data instanceof \Traversable && $data instanceof \ArrayAccess); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/EventListener/ScalarCollectionEntryFactory.php b/src/Oro/Bundle/ApiBundle/Form/EventListener/ScalarCollectionEntryFactory.php new file mode 100644 index 00000000000..8c0177c397c --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/EventListener/ScalarCollectionEntryFactory.php @@ -0,0 +1,64 @@ +dataProperty = $dataProperty; + } + + /** + * {@inheritdoc} + */ + public function createEntry(FormFactoryInterface $factory, $name) + { + $entryTypeBuilder = $factory->createNamedBuilder( + $name, + FormType::class, + null, + [ + 'auto_initialize' => false, + 'data_class' => $this->dataClass, + 'property_path' => '[' . $name . ']', + 'error_bubbling' => false, + 'constraints' => new Assert\Valid() + ] + ); + $entryTypeBuilder->addEventListener( + FormEvents::PRE_SUBMIT, + function (FormEvent $event) { + $event->setData([$this->dataProperty => $event->getData()]); + } + ); + + $entryType = $entryTypeBuilder->getForm(); + $entryType->add( + $this->dataProperty, + $this->type, + array_replace( + ['error_bubbling' => true], + $this->options + ) + ); + + return $entryType; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/Extension/CustomizeFormDataExtension.php b/src/Oro/Bundle/ApiBundle/Form/Extension/CustomizeFormDataExtension.php new file mode 100644 index 00000000000..d402351899b --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/Extension/CustomizeFormDataExtension.php @@ -0,0 +1,187 @@ +customizationProcessor = $customizationProcessor; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (empty($options['data_class'])) { + return; + } + + if (array_key_exists(self::API_CONTEXT, $options)) { + $builder->setAttribute(self::API_CONTEXT, $options[self::API_CONTEXT]); + } + + $this->addEventListeners($builder); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefined([self::API_CONTEXT]) + ->setAllowedTypes(self::API_CONTEXT, ['null', FormContext::class]); + } + + /** + * {@inheritdoc} + */ + public function getExtendedType() + { + return FormType::class; + } + + /** + * @param FormBuilderInterface $builder + */ + protected function addEventListeners(FormBuilderInterface $builder) + { + // the same context object is used for all listeners to allow sharing the data between them + $builder->setAttribute(self::API_EVENT_CONTEXT, $this->customizationProcessor->createContext()); + + $builder->addEventListener( + FormEvents::PRE_SUBMIT, + function (FormEvent $event) { + $context = $this->handleFormEvent(CustomizeFormDataContext::EVENT_PRE_SUBMIT, $event); + if (null !== $context) { + $event->setData($context->getData()); + } + }, + -255 // this listener should be executed after all other listeners + ); + $builder->addEventListener( + FormEvents::SUBMIT, + function (FormEvent $event) { + $context = $this->handleFormEvent(CustomizeFormDataContext::EVENT_SUBMIT, $event); + if (null !== $context) { + $event->setData($context->getData()); + } + }, + -255 // this listener should be executed after all other listeners + ); + $builder->addEventListener( + FormEvents::POST_SUBMIT, + function (FormEvent $event) { + $this->handleFormEvent(CustomizeFormDataContext::EVENT_POST_SUBMIT, $event); + }, + 255 // this listener should be executed before all other listeners, including the validation one + ); + $builder->addEventListener( + FormEvents::POST_SUBMIT, + function (FormEvent $event) { + $this->handleFormEvent(CustomizeFormDataContext::EVENT_FINISH_SUBMIT, $event); + }, + -255 // this listener should be executed after all other listeners, including the validation one + ); + } + + /** + * @param string $eventName + * @param FormEvent $event + * + * @return CustomizeFormDataContext|null + */ + protected function handleFormEvent($eventName, FormEvent $event) + { + $context = $this->getInitializedContext($event->getForm()); + if (null !== $context) { + $context->setEvent($eventName); + $context->setData($event->getData()); + $this->customizationProcessor->process($context); + } + + return $context; + } + + /** + * @param FormInterface $form + * + * @return CustomizeFormDataContext|null + */ + protected function getInitializedContext(FormInterface $form) + { + /** @var CustomizeFormDataContext $context */ + $context = $form->getConfig()->getAttribute(self::API_EVENT_CONTEXT); + if ($context->has(CustomizeFormDataContext::CLASS_NAME)) { + // already initialized + return $context; + } + + $rootFormConfig = $form->getRoot()->getConfig(); + if (!$rootFormConfig->hasAttribute(self::API_CONTEXT)) { + // by some reasons the root form does not have the context of API action + return null; + } + + /** @var FormContext $formContext */ + $formContext = $rootFormConfig->getAttribute(self::API_CONTEXT); + $context->setVersion($formContext->getVersion()); + $context->getRequestType()->set($formContext->getRequestType()); + $context->setConfig($formContext->getConfig()); + $context->setClassName($form->getConfig()->getDataClass()); + $context->setForm($form); + if (null !== $form->getParent()) { + $context->setRootClassName($rootFormConfig->getDataClass()); + $context->setPropertyPath($this->getPropertyPath($form)); + } + + return $context; + } + + /** + * @param FormInterface $form + * + * @return string + */ + protected function getPropertyPath(FormInterface $form) + { + $path = []; + while (null !== $form->getParent()->getParent()) { + if (!$form->getData() instanceof Collection) { + if ($form->getParent()->getData() instanceof Collection) { + $path[] = $form->getParent()->getName(); + } else { + $path[] = $form->getName(); + } + } + $form = $form->getParent(); + } + + return implode('.', array_reverse($path)); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/Guesser/MetadataTypeGuesser.php b/src/Oro/Bundle/ApiBundle/Form/Guesser/MetadataTypeGuesser.php index 66160a8176a..6b45609f014 100644 --- a/src/Oro/Bundle/ApiBundle/Form/Guesser/MetadataTypeGuesser.php +++ b/src/Oro/Bundle/ApiBundle/Form/Guesser/MetadataTypeGuesser.php @@ -5,24 +5,37 @@ use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\Guess\TypeGuess; +use Oro\Bundle\ApiBundle\Config\ConfigAccessorInterface; +use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; +use Oro\Bundle\ApiBundle\Config\EntityDefinitionFieldConfig; use Oro\Bundle\ApiBundle\Metadata\AssociationMetadata; use Oro\Bundle\ApiBundle\Metadata\EntityMetadata; use Oro\Bundle\ApiBundle\Metadata\MetadataAccessorInterface; +use Oro\Bundle\ApiBundle\Request\DataType; +use Oro\Bundle\ApiBundle\Util\DoctrineHelper; class MetadataTypeGuesser implements FormTypeGuesserInterface { + /** @var array [data type => [form type, options], ...] */ + protected $dataTypeMappings = []; + + /** @var DoctrineHelper */ + protected $doctrineHelper; + /** @var MetadataAccessorInterface|null */ protected $metadataAccessor; - /** @var array [data type => [form type, options], ...] */ - protected $dataTypeMappings = []; + /** @var ConfigAccessorInterface|null */ + protected $configAccessor; /** - * @param array $dataTypeMappings [data type => [form type, options], ...] + * @param array $dataTypeMappings [data type => [form type, options], ...] + * @param DoctrineHelper $doctrineHelper */ - public function __construct(array $dataTypeMappings = []) + public function __construct(array $dataTypeMappings, DoctrineHelper $doctrineHelper) { $this->dataTypeMappings = $dataTypeMappings; + $this->doctrineHelper = $doctrineHelper; } /** @@ -33,6 +46,14 @@ public function setMetadataAccessor(MetadataAccessorInterface $metadataAccessor $this->metadataAccessor = $metadataAccessor; } + /** + * @param ConfigAccessorInterface|null $configAccessor + */ + public function setConfigAccessor(ConfigAccessorInterface $configAccessor = null) + { + $this->configAccessor = $configAccessor; + } + /** * @param string $dataType * @param string $formType @@ -53,7 +74,17 @@ public function guessType($class, $property) if ($metadata->hasField($property)) { return $this->getTypeGuessForField($metadata->getField($property)->getDataType()); } elseif ($metadata->hasAssociation($property)) { - return $this->getTypeGuessForAssociation($metadata->getAssociation($property)); + $association = $metadata->getAssociation($property); + if (DataType::isAssociationAsField($association->getDataType())) { + return $association->isCollapsed() + ? $this->getTypeGuessForCollapsedArrayAssociation($association) + : $this->getTypeGuessForArrayAssociation( + $association, + $this->getConfigForClass($class)->getField($property) + ); + } + + return $this->getTypeGuessForAssociation($association); } } @@ -96,6 +127,18 @@ protected function getMetadataForClass($class) : null; } + /** + * @param string $class + * + * @return EntityDefinitionConfig|null + */ + protected function getConfigForClass($class) + { + return null !== $this->configAccessor + ? $this->configAccessor->getConfig($class) + : null; + } + /** * @param string $formType * @param array $formOptions @@ -145,4 +188,74 @@ protected function getTypeGuessForAssociation(AssociationMetadata $metadata) TypeGuess::HIGH_CONFIDENCE ); } + + /** + * @param AssociationMetadata $metadata + * @param EntityDefinitionFieldConfig $config + * + * @return TypeGuess|null + */ + protected function getTypeGuessForArrayAssociation( + AssociationMetadata $metadata, + EntityDefinitionFieldConfig $config + ) { + $targetMetadata = $metadata->getTargetMetadata(); + if (null === $targetMetadata) { + return null; + } + + $formType = $this->doctrineHelper->isManageableEntityClass($targetMetadata->getClassName()) + ? 'oro_api_entity_collection' + : 'oro_api_collection'; + + return $this->createTypeGuess( + $formType, + [ + 'entry_data_class' => $targetMetadata->getClassName(), + 'entry_type' => 'oro_api_compound_entity', + 'entry_options' => [ + 'metadata' => $targetMetadata, + 'config' => $config->getTargetEntity() + ] + ], + TypeGuess::HIGH_CONFIDENCE + ); + } + + /** + * @param AssociationMetadata $metadata + * + * @return TypeGuess|null + */ + protected function getTypeGuessForCollapsedArrayAssociation(AssociationMetadata $metadata) + { + $targetMetadata = $metadata->getTargetMetadata(); + if (null === $targetMetadata) { + return null; + } + + // it is expected that collapsed association must have only one field or association + $fieldNames = array_keys($targetMetadata->getFields()); + $targetFieldName = reset($fieldNames); + if (!$targetFieldName) { + $associationNames = array_keys($targetMetadata->getAssociations()); + $targetFieldName = reset($associationNames); + } + if (!$targetFieldName) { + return null; + } + + $formType = $this->doctrineHelper->isManageableEntityClass($targetMetadata->getClassName()) + ? 'oro_api_entity_scalar_collection' + : 'oro_api_scalar_collection'; + + return $this->createTypeGuess( + $formType, + [ + 'entry_data_class' => $targetMetadata->getClassName(), + 'entry_data_property' => $targetFieldName, + ], + TypeGuess::HIGH_CONFIDENCE + ); + } } diff --git a/src/Oro/Bundle/ApiBundle/Form/ReflectionUtil.php b/src/Oro/Bundle/ApiBundle/Form/ReflectionUtil.php index 8514b9fb536..5f65ed1bd3f 100644 --- a/src/Oro/Bundle/ApiBundle/Form/ReflectionUtil.php +++ b/src/Oro/Bundle/ApiBundle/Form/ReflectionUtil.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\ApiBundle\Form; +use Symfony\Component\Form\FormInterface; use Symfony\Component\PropertyAccess\StringUtil; class ReflectionUtil @@ -53,6 +54,31 @@ public static function findAdderAndRemover($object, $property) return null; } + /** + * Removes all errors of the given form. + * + * @param FormInterface $form The form + * @param bool $deep Whether to clear errors of child forms as well + */ + public static function clearFormErrors(FormInterface $form, $deep = false) + { + if (count($form->getErrors()) > 0) { + $clearClosure = \Closure::bind( + function (FormInterface $form) { + $form->errors = []; + }, + null, + $form + ); + $clearClosure($form); + } + if ($deep) { + foreach ($form as $childForm) { + self::clearFormErrors($childForm); + } + } + } + /** * Returns whether a method is public and has the number of required parameters. * diff --git a/src/Oro/Bundle/ApiBundle/Form/Type/CollectionType.php b/src/Oro/Bundle/ApiBundle/Form/Type/CollectionType.php new file mode 100644 index 00000000000..fac8d3f4cc8 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/Type/CollectionType.php @@ -0,0 +1,60 @@ +addEventSubscriber( + new CollectionListener( + new CollectionEntryFactory( + $options['entry_data_class'], + $options['entry_type'], + $options['entry_options'] + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'by_reference' => false, + 'entry_options' => [], + ]); + $resolver->setRequired([ + 'entry_type', + 'entry_data_class' + ]); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->getBlockPrefix(); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'oro_api_collection'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/Type/CompoundEntityType.php b/src/Oro/Bundle/ApiBundle/Form/Type/CompoundEntityType.php new file mode 100644 index 00000000000..53159520cf6 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/Type/CompoundEntityType.php @@ -0,0 +1,98 @@ +getFields(); + foreach ($fields as $name => $field) { + $fieldConfig = $config->getField($name); + $builder->add( + $name, + $fieldConfig->getFormType(), + $this->getFormFieldOptions($fieldConfig) + ); + } + $associations = $metadata->getAssociations(); + foreach ($associations as $name => $association) { + if (DataType::isAssociationAsField($association->getDataType())) { + $fieldConfig = $config->getField($name); + $builder->add( + $name, + $fieldConfig->getFormType(), + $this->getFormFieldOptions($fieldConfig) + ); + } + } + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setRequired(['metadata', 'config']) + ->setAllowedTypes('metadata', ['Oro\Bundle\ApiBundle\Metadata\EntityMetadata']) + ->setAllowedTypes('config', ['Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig']); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->getBlockPrefix(); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'oro_api_compound_entity'; + } + + /** + * @param EntityDefinitionFieldConfig $fieldConfig + * + * @return array + */ + protected function getFormFieldOptions(EntityDefinitionFieldConfig $fieldConfig) + { + $options = $fieldConfig->getFormOptions(); + if (null === $options) { + $options = []; + } + $propertyPath = $fieldConfig->getPropertyPath(); + if ($propertyPath) { + if (ConfigUtil::IGNORE_PROPERTY_PATH === $propertyPath) { + $options['mapped'] = false; + } else { + $options['property_path'] = $propertyPath; + } + } + + return $options; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/Type/EntityCollectionType.php b/src/Oro/Bundle/ApiBundle/Form/Type/EntityCollectionType.php new file mode 100644 index 00000000000..edfd1b3e856 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/Type/EntityCollectionType.php @@ -0,0 +1,42 @@ +addEventSubscriber(new MergeDoctrineCollectionListener()); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->getBlockPrefix(); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'oro_api_entity_collection'; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_api_collection'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/Type/EntityScalarCollectionType.php b/src/Oro/Bundle/ApiBundle/Form/Type/EntityScalarCollectionType.php new file mode 100644 index 00000000000..5c06e757777 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/Type/EntityScalarCollectionType.php @@ -0,0 +1,42 @@ +addEventSubscriber(new MergeDoctrineCollectionListener()); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->getBlockPrefix(); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'oro_api_entity_scalar_collection'; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_api_scalar_collection'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Form/Type/ScalarCollectionType.php b/src/Oro/Bundle/ApiBundle/Form/Type/ScalarCollectionType.php new file mode 100644 index 00000000000..3965f832f5a --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Form/Type/ScalarCollectionType.php @@ -0,0 +1,62 @@ +addEventSubscriber( + new CollectionListener( + new ScalarCollectionEntryFactory( + $options['entry_data_class'], + $options['entry_data_property'], + $options['entry_type'], + $options['entry_options'] + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'by_reference' => false, + 'entry_type' => 'text', + 'entry_options' => [] + ]); + $resolver->setRequired([ + 'entry_data_class', + 'entry_data_property' + ]); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->getBlockPrefix(); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'oro_api_scalar_collection'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Metadata/AssociationMetadata.php b/src/Oro/Bundle/ApiBundle/Metadata/AssociationMetadata.php index d9f0b72385b..8fc3f3eb652 100644 --- a/src/Oro/Bundle/ApiBundle/Metadata/AssociationMetadata.php +++ b/src/Oro/Bundle/ApiBundle/Metadata/AssociationMetadata.php @@ -27,6 +27,9 @@ class AssociationMetadata implements ToArrayInterface /** @var bool */ protected $nullable = false; + /** @var bool */ + protected $collapsed = false; + /** @var EntityMetadata|null */ private $targetMetadata; @@ -55,25 +58,20 @@ public function __clone() */ public function toArray() { - $result = ['name' => $this->name]; - if ($this->dataType) { - $result['data_type'] = $this->dataType; - } + $result = [ + 'name' => $this->name, + 'data_type' => $this->dataType, + 'nullable' => $this->nullable, + 'collapsed' => $this->collapsed, + 'association_type' => $this->associationType, + 'collection' => $this->collection + ]; if ($this->targetClass) { $result['target_class'] = $this->targetClass; } if ($this->acceptableTargetClasses) { $result['acceptable_target_classes'] = $this->acceptableTargetClasses; } - if ($this->associationType) { - $result['association_type'] = $this->associationType; - } - if ($this->collection) { - $result['collection'] = $this->collection; - } - if ($this->nullable) { - $result['nullable'] = $this->nullable; - } if (null !== $this->targetMetadata) { $result['target_metadata'] = $this->targetMetadata->toArray(); } @@ -270,4 +268,24 @@ public function setIsNullable($value) { $this->nullable = $value; } + + /** + * Indicates whether the association should be collapsed to a scalar. + * + * @return bool + */ + public function isCollapsed() + { + return $this->collapsed; + } + + /** + * Sets a flag indicates whether the association should be collapsed to a scalar. + * + * @param bool $collapsed + */ + public function setCollapsed($collapsed = true) + { + $this->collapsed = $collapsed; + } } diff --git a/src/Oro/Bundle/ApiBundle/Normalizer/ConfigNormalizer.php b/src/Oro/Bundle/ApiBundle/Normalizer/ConfigNormalizer.php new file mode 100644 index 00000000000..4f63d27d3d5 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Normalizer/ConfigNormalizer.php @@ -0,0 +1,76 @@ +getFields(); + foreach ($fields as $fieldName => $field) { + $propertyPath = $field->getPropertyPath(); + if (ConfigUtil::IGNORE_PROPERTY_PATH === $propertyPath) { + $toRemove[] = $fieldName; + } + if ($field->getDependsOn() && !$field->isExcluded()) { + $this->processDependentFields($config, $field->getDependsOn()); + } + } + foreach ($toRemove as $fieldName) { + $config->removeField($fieldName); + } + foreach ($fields as $field) { + $targetConfig = $field->getTargetEntity(); + if (null !== $targetConfig) { + $this->normalizeConfig($targetConfig); + } + } + } + + /** + * @param EntityDefinitionConfig $config + * @param string[] $dependsOn + */ + protected function processDependentFields(EntityDefinitionConfig $config, array $dependsOn) + { + foreach ($dependsOn as $dependsOnPropertyPath) { + $this->processDependentField($config, ConfigUtil::explodePropertyPath($dependsOnPropertyPath)); + } + } + + /** + * @param EntityDefinitionConfig $config + * @param string[] $dependsOnPropertyPath + */ + protected function processDependentField(EntityDefinitionConfig $config, array $dependsOnPropertyPath) + { + $dependsOnFieldName = $dependsOnPropertyPath[0]; + $dependsOnField = $config->getOrAddField($dependsOnFieldName); + if ($dependsOnField->isExcluded()) { + $dependsOnField->setExcluded(false); + $dependsOn = $dependsOnField->getDependsOn(); + if ($dependsOn) { + $this->processDependentFields($config, $dependsOn); + } + } + if (count($dependsOnPropertyPath) > 1) { + $targetConfig = $dependsOnField->getOrCreateTargetEntity(); + $this->processDependentField($targetConfig, array_slice($dependsOnPropertyPath, 1)); + } + } +} diff --git a/src/Oro/Bundle/ApiBundle/Normalizer/ObjectNormalizer.php b/src/Oro/Bundle/ApiBundle/Normalizer/ObjectNormalizer.php index da434bd8d74..06242813d99 100644 --- a/src/Oro/Bundle/ApiBundle/Normalizer/ObjectNormalizer.php +++ b/src/Oro/Bundle/ApiBundle/Normalizer/ObjectNormalizer.php @@ -19,6 +19,9 @@ class ObjectNormalizer { const MAX_NESTING_LEVEL = 1; + /** @var ObjectNormalizerRegistry */ + protected $normalizerRegistry; + /** @var DoctrineHelper */ protected $doctrineHelper; @@ -28,25 +31,28 @@ class ObjectNormalizer /** @var DataTransformerInterface */ protected $dataTransformer; - /** @var ObjectNormalizerRegistry */ - private $normalizerRegistry; + /** @var ConfigNormalizer */ + protected $configNormalizer; /** * @param ObjectNormalizerRegistry $normalizerRegistry * @param DoctrineHelper $doctrineHelper * @param DataAccessorInterface $dataAccessor * @param DataTransformerInterface $dataTransformer + * @param ConfigNormalizer $configNormalizer */ public function __construct( ObjectNormalizerRegistry $normalizerRegistry, DoctrineHelper $doctrineHelper, DataAccessorInterface $dataAccessor, - DataTransformerInterface $dataTransformer + DataTransformerInterface $dataTransformer, + ConfigNormalizer $configNormalizer ) { $this->normalizerRegistry = $normalizerRegistry; $this->doctrineHelper = $doctrineHelper; $this->dataAccessor = $dataAccessor; $this->dataTransformer = $dataTransformer; + $this->configNormalizer = $configNormalizer; } /** @@ -58,6 +64,11 @@ public function __construct( public function normalizeObject($object, EntityDefinitionConfig $config = null) { if (null !== $object) { + if (null !== $config) { + $normalizedConfig = clone $config; + $this->configNormalizer->normalizeConfig($normalizedConfig); + $config = $normalizedConfig; + } $object = $this->normalizeValue($object, is_array($object) ? 0 : 1, $config); } @@ -148,7 +159,7 @@ protected function normalizeObjectByConfig($object, $level, EntityDefinitionConf } $value = null; - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if (ConfigUtil::isMetadataProperty($propertyPath)) { $value = $this->getMetadataProperty($entityClass, $propertyPath); } elseif ($this->dataAccessor->tryGetValue($object, $propertyPath, $value) && null !== $value) { diff --git a/src/Oro/Bundle/ApiBundle/Processor/CollectResources/CollectResourcesContext.php b/src/Oro/Bundle/ApiBundle/Processor/CollectResources/CollectResourcesContext.php index ded92b9ca36..f3d4b83ff54 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/CollectResources/CollectResourcesContext.php +++ b/src/Oro/Bundle/ApiBundle/Processor/CollectResources/CollectResourcesContext.php @@ -11,6 +11,9 @@ */ class CollectResourcesContext extends ApiContext { + /** @var string[] */ + protected $accessibleResources = []; + /** * {@inheritdoc} */ @@ -19,4 +22,24 @@ protected function initialize() parent::initialize(); $this->setResult(new ApiResourceCollection()); } + + /** + * Gets a list of resources accessible through Data API. + * + * @return string[] The list of class names + */ + public function getAccessibleResources() + { + return $this->accessibleResources; + } + + /** + * Sets a list of resources accessible through Data API. + * + * @param string[] $classNames + */ + public function setAccessibleResources(array $classNames) + { + $this->accessibleResources = $classNames; + } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/CollectResources/LoadAccessibleResources.php b/src/Oro/Bundle/ApiBundle/Processor/CollectResources/LoadAccessibleResources.php new file mode 100644 index 00000000000..29a8702d4ad --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CollectResources/LoadAccessibleResources.php @@ -0,0 +1,35 @@ +getAccessibleResources(); + if (!empty($accessibleResources)) { + // the accessible resources are already built + return; + } + + $resources = $context->getResult(); + foreach ($resources as $resource) { + if (!in_array(ApiActions::GET, $resource->getExcludedActions(), true)) { + $accessibleResources[] = $resource->getEntityClass(); + } + } + $context->setAccessibleResources($accessibleResources); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/CollectSubresourcesContext.php b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/CollectSubresourcesContext.php index 417becaf7bd..2c969bf3b42 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/CollectSubresourcesContext.php +++ b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/CollectSubresourcesContext.php @@ -15,6 +15,9 @@ class CollectSubresourcesContext extends ApiContext /** @var ApiResource[] [entity class => ApiResource, ... ] */ protected $resources = []; + /** @var string[] */ + protected $accessibleResources = []; + /** * {@inheritdoc} */ @@ -72,4 +75,24 @@ public function setResources(array $resources) $this->resources[$resource->getEntityClass()] = $resource; } } + + /** + * Gets a list of resources accessible through Data API. + * + * @return string[] The list of class names + */ + public function getAccessibleResources() + { + return $this->accessibleResources; + } + + /** + * Sets a list of resources accessible through Data API. + * + * @param string[] $classNames + */ + public function setAccessibleResources(array $classNames) + { + $this->accessibleResources = $classNames; + } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/InitializeSubresources.php b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/InitializeSubresources.php index f83e4aae247..109ad3882a3 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/InitializeSubresources.php +++ b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/InitializeSubresources.php @@ -3,14 +3,7 @@ namespace Oro\Bundle\ApiBundle\Processor\CollectSubresources; use Oro\Component\ChainProcessor\ContextInterface; -use Oro\Component\ChainProcessor\ProcessorInterface; -use Oro\Bundle\ApiBundle\Config\ConfigExtraInterface; -use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfigExtra; use Oro\Bundle\ApiBundle\Exception\RuntimeException; -use Oro\Bundle\ApiBundle\Metadata\MetadataExtraInterface; -use Oro\Bundle\ApiBundle\Provider\ConfigProvider; -use Oro\Bundle\ApiBundle\Provider\MetadataProvider; -use Oro\Bundle\ApiBundle\Request\ApiActions; use Oro\Bundle\ApiBundle\Request\ApiResource; use Oro\Bundle\ApiBundle\Request\ApiResourceSubresources; use Oro\Bundle\ApiBundle\Request\RequestType; @@ -18,24 +11,8 @@ /** * Initializes sub-resources for all API resources based on API configuration and metadata. */ -class InitializeSubresources implements ProcessorInterface +class InitializeSubresources extends LoadSubresources { - /** @var ConfigProvider */ - protected $configProvider; - - /** @var MetadataProvider */ - protected $metadataProvider; - - /** - * @param ConfigProvider $configProvider - * @param MetadataProvider $metadataProvider - */ - public function __construct(ConfigProvider $configProvider, MetadataProvider $metadataProvider) - { - $this->configProvider = $configProvider; - $this->metadataProvider = $metadataProvider; - } - /** * {@inheritdoc} */ @@ -51,45 +28,21 @@ public function process(ContextInterface $context) $version = $context->getVersion(); $requestType = $context->getRequestType(); - $configExtras = $this->getConfigExtras(); - $metadataExtras = $this->getMetadataExtras(); + $accessibleResources = array_fill_keys($context->getAccessibleResources(), true); $resources = $context->getResources(); foreach ($resources as $resource) { $subresources->add( - $this->createEntitySubresources( - $resource, - $version, - $requestType, - $configExtras, - $metadataExtras - ) + $this->createEntitySubresources($resource, $version, $requestType, $accessibleResources) ); } } /** - * @return ConfigExtraInterface[] - */ - protected function getConfigExtras() - { - return [new EntityDefinitionConfigExtra()]; - } - - /** - * @return MetadataExtraInterface[] - */ - protected function getMetadataExtras() - { - return []; - } - - /** - * @param ApiResource $resource - * @param string $version - * @param RequestType $requestType - * @param ConfigExtraInterface[] $configExtras - * @param MetadataExtraInterface[] $metadataExtras + * @param ApiResource $resource + * @param string $version + * @param RequestType $requestType + * @param array $accessibleResources * * @return ApiResourceSubresources */ @@ -97,87 +50,29 @@ protected function createEntitySubresources( ApiResource $resource, $version, RequestType $requestType, - array $configExtras, - array $metadataExtras + array $accessibleResources ) { $entityClass = $resource->getEntityClass(); - $config = $this->configProvider->getConfig( - $entityClass, - $version, - $requestType, - $configExtras - ); - $metadata = $this->metadataProvider->getMetadata( - $entityClass, - $version, - $requestType, - $config->getDefinition(), - $metadataExtras - ); + $config = $this->getConfig($entityClass, $version, $requestType); + $metadata = $this->getMetadata($entityClass, $version, $requestType, $config); if (null === $metadata) { throw new RuntimeException(sprintf('A metadata for "%s" entity does not exist.', $entityClass)); } - $resourceExcludedActions = $resource->getExcludedActions(); - $subresourceExcludedActions = !empty($resourceExcludedActions) - ? $this->getSubresourceExcludedActions($resourceExcludedActions) - : []; - + $subresourceExcludedActions = $this->getSubresourceExcludedActions($resource); $entitySubresources = new ApiResourceSubresources($entityClass); $associations = $metadata->getAssociations(); foreach ($associations as $associationName => $association) { - $subresource = $entitySubresources->addSubresource($associationName); - $subresource->setTargetClassName($association->getTargetClassName()); - $subresource->setAcceptableTargetClassNames($association->getAcceptableTargetClassNames()); - $subresource->setIsCollection($association->isCollection()); - if (!$association->isCollection()) { - $excludedActions = $subresourceExcludedActions; - if (!in_array(ApiActions::ADD_RELATIONSHIP, $excludedActions, true)) { - $excludedActions[] = ApiActions::ADD_RELATIONSHIP; - } - if (!in_array(ApiActions::DELETE_RELATIONSHIP, $excludedActions, true)) { - $excludedActions[] = ApiActions::DELETE_RELATIONSHIP; - } - $subresource->setExcludedActions($excludedActions); - } elseif (!empty($subresourceExcludedActions)) { - $subresource->setExcludedActions($subresourceExcludedActions); + if ($this->isExcludedAssociation($associationName, $config)) { + continue; } - } - - return $entitySubresources; - } - /** - * @param string[] $resourceExcludedActions - * - * @return string[] - */ - protected function getSubresourceExcludedActions(array $resourceExcludedActions) - { - $result = array_intersect( - $resourceExcludedActions, - [ - ApiActions::GET_SUBRESOURCE, - ApiActions::GET_RELATIONSHIP, - ApiActions::UPDATE_RELATIONSHIP, - ApiActions::ADD_RELATIONSHIP, - ApiActions::DELETE_RELATIONSHIP - ] - ); - - if (in_array(ApiActions::UPDATE, $resourceExcludedActions, true)) { - $result = array_unique( - array_merge( - $result, - [ - ApiActions::UPDATE_RELATIONSHIP, - ApiActions::ADD_RELATIONSHIP, - ApiActions::DELETE_RELATIONSHIP - ] - ) + $entitySubresources->addSubresource( + $associationName, + $this->createSubresource($association, $accessibleResources, $subresourceExcludedActions) ); } - return array_values($result); + return $entitySubresources; } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/LoadFromConfigBag.php b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/LoadFromConfigBag.php index ece2ddf678d..ac4708cce33 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/LoadFromConfigBag.php +++ b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/LoadFromConfigBag.php @@ -3,7 +3,11 @@ namespace Oro\Bundle\ApiBundle\Processor\CollectSubresources; use Oro\Component\ChainProcessor\ContextInterface; -use Oro\Component\ChainProcessor\ProcessorInterface; +use Oro\Bundle\ApiBundle\Config\EntityDefinitionFieldConfig; +use Oro\Bundle\ApiBundle\Metadata\AssociationMetadata; +use Oro\Bundle\ApiBundle\Provider\ConfigProvider; +use Oro\Bundle\ApiBundle\Provider\MetadataProvider; +use Oro\Bundle\ApiBundle\Request\RequestType; use Oro\Bundle\ApiBundle\Config\SubresourceConfig; use Oro\Bundle\ApiBundle\Config\SubresourcesConfig; use Oro\Bundle\ApiBundle\Config\ConfigLoaderFactory; @@ -16,7 +20,7 @@ /** * Loads sub-resources configured in "Resources/config/oro/api.yml". */ -class LoadFromConfigBag implements ProcessorInterface +class LoadFromConfigBag extends LoadSubresources { /** @var ConfigLoaderFactory */ protected $configLoaderFactory; @@ -27,11 +31,16 @@ class LoadFromConfigBag implements ProcessorInterface /** * @param ConfigLoaderFactory $configLoaderFactory * @param ConfigBag $configBag + * @param ConfigProvider $configProvider + * @param MetadataProvider $metadataProvider */ public function __construct( ConfigLoaderFactory $configLoaderFactory, - ConfigBag $configBag + ConfigBag $configBag, + ConfigProvider $configProvider, + MetadataProvider $metadataProvider ) { + parent::__construct($configProvider, $metadataProvider); $this->configLoaderFactory = $configLoaderFactory; $this->configBag = $configBag; } @@ -44,8 +53,11 @@ public function process(ContextInterface $context) /** @var CollectSubresourcesContext $context */ $version = $context->getVersion(); - $resources = $context->getResources(); + $requestType = $context->getRequestType(); + $accessibleResources = array_fill_keys($context->getAccessibleResources(), true); $subresources = $context->getResult(); + + $resources = $context->getResources(); foreach ($resources as $entityClass => $resource) { if (in_array(ApiActions::GET_SUBRESOURCE, $resource->getExcludedActions(), true)) { continue; @@ -71,7 +83,16 @@ public function process(ContextInterface $context) } else { $subresource = $entitySubresources->getSubresource($associationName); if (null === $subresource) { - $subresource = $this->createNewSubresource($subresourceConfig); + if ($subresourceConfig->getTargetClass()) { + $subresource = $this->createSubresourceFromConfig($subresourceConfig); + } else { + $subresource = $this->createSubresource( + $this->getAssociationMetadata($entityClass, $associationName, $version, $requestType), + $accessibleResources, + $this->getSubresourceExcludedActions($resource) + ); + $this->updateSubresourceTargetFromConfig($subresource, $subresourceConfig); + } $entitySubresources->addSubresource($associationName, $subresource); } else { $this->validateExistingSubresource( @@ -92,7 +113,7 @@ public function process(ContextInterface $context) * * @return ApiSubresource */ - protected function createNewSubresource(SubresourceConfig $subresourceConfig) + protected function createSubresourceFromConfig(SubresourceConfig $subresourceConfig) { $subresource = new ApiSubresource(); $subresource->setTargetClassName($subresourceConfig->getTargetClass()); @@ -102,6 +123,55 @@ protected function createNewSubresource(SubresourceConfig $subresourceConfig) return $subresource; } + /** + * @param ApiSubresource $subresource + * @param SubresourceConfig $subresourceConfig + */ + protected function updateSubresourceTargetFromConfig( + ApiSubresource $subresource, + SubresourceConfig $subresourceConfig + ) { + $targetClass = $subresourceConfig->getTargetClass(); + if ($targetClass) { + $subresource->setTargetClassName($targetClass); + $subresource->setAcceptableTargetClassNames([$targetClass]); + } + if ($subresourceConfig->hasTargetType()) { + $subresource->setIsCollection($subresourceConfig->isCollectionValuedAssociation()); + } + } + + /** + * @param string $entityClass + * @param string $associationName + * @param string $version + * @param RequestType $requestType + * + * @return AssociationMetadata + */ + protected function getAssociationMetadata($entityClass, $associationName, $version, RequestType $requestType) + { + $metadata = $this->getMetadata( + $entityClass, + $version, + $requestType, + $this->getConfig($entityClass, $version, $requestType) + ); + + $association = $metadata->getAssociation($associationName); + if (null === $association) { + throw new \RuntimeException( + sprintf( + 'The target class for "%s" subresource of "%s" entity should be specified in config.', + $associationName, + $entityClass + ) + ); + } + + return $association; + } + /** * @param string $entityClass * @param string $associationName diff --git a/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/LoadSubresources.php b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/LoadSubresources.php new file mode 100644 index 00000000000..7dc010234cf --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CollectSubresources/LoadSubresources.php @@ -0,0 +1,232 @@ +configProvider = $configProvider; + $this->metadataProvider = $metadataProvider; + } + + /** + * @param AssociationMetadata $association + * @param array $accessibleResources + * @param array $subresourceExcludedActions + * + * @return ApiSubresource + */ + protected function createSubresource( + AssociationMetadata $association, + array $accessibleResources, + array $subresourceExcludedActions + ) { + $subresource = new ApiSubresource(); + $subresource->setTargetClassName($association->getTargetClassName()); + $subresource->setAcceptableTargetClassNames($association->getAcceptableTargetClassNames()); + $subresource->setIsCollection($association->isCollection()); + if ($association->isCollection()) { + if (!$this->isAccessibleAssociation($association, $accessibleResources)) { + $subresource->setExcludedActions($this->getToManyRelationshipsActions()); + } elseif (!empty($subresourceExcludedActions)) { + $subresource->setExcludedActions($subresourceExcludedActions); + } + } else { + if (!$this->isAccessibleAssociation($association, $accessibleResources)) { + $subresource->setExcludedActions($this->getToOneRelationshipsActions()); + } else { + $excludedActions = $subresourceExcludedActions; + if (!in_array(ApiActions::ADD_RELATIONSHIP, $excludedActions, true)) { + $excludedActions[] = ApiActions::ADD_RELATIONSHIP; + } + if (!in_array(ApiActions::DELETE_RELATIONSHIP, $excludedActions, true)) { + $excludedActions[] = ApiActions::DELETE_RELATIONSHIP; + } + $subresource->setExcludedActions($excludedActions); + } + } + + return $subresource; + } + + /** + * @param ApiResource $resource + * + * @return string[] + */ + protected function getSubresourceExcludedActions(ApiResource $resource) + { + $resourceExcludedActions = $resource->getExcludedActions(); + if (empty($resourceExcludedActions)) { + return []; + } + + $result = array_intersect( + $resourceExcludedActions, + [ + ApiActions::GET_SUBRESOURCE, + ApiActions::GET_RELATIONSHIP, + ApiActions::UPDATE_RELATIONSHIP, + ApiActions::ADD_RELATIONSHIP, + ApiActions::DELETE_RELATIONSHIP + ] + ); + + if (in_array(ApiActions::UPDATE, $resourceExcludedActions, true)) { + $result = array_unique( + array_merge( + $result, + [ + ApiActions::UPDATE_RELATIONSHIP, + ApiActions::ADD_RELATIONSHIP, + ApiActions::DELETE_RELATIONSHIP + ] + ) + ); + } + + return array_values($result); + } + + /** + * @return string[] + */ + protected function getToOneRelationshipsActions() + { + return [ + ApiActions::GET_SUBRESOURCE, + ApiActions::GET_RELATIONSHIP, + ApiActions::UPDATE_RELATIONSHIP + ]; + } + + /** + * @return string[] + */ + protected function getToManyRelationshipsActions() + { + return [ + ApiActions::GET_SUBRESOURCE, + ApiActions::GET_RELATIONSHIP, + ApiActions::UPDATE_RELATIONSHIP, + ApiActions::ADD_RELATIONSHIP, + ApiActions::DELETE_RELATIONSHIP + ]; + } + + /** + * @param string $fieldName + * @param EntityDefinitionConfig|null $config + * + * @return bool + */ + protected function isExcludedAssociation($fieldName, EntityDefinitionConfig $config = null) + { + if (null === $config) { + return false; + } + $field = $config->getField($fieldName); + if (null === $field) { + return false; + } + + return $field->isExcluded(); + } + + /** + * @param AssociationMetadata $association + * @param array $accessibleResources + * + * @return bool + */ + protected function isAccessibleAssociation(AssociationMetadata $association, array $accessibleResources) + { + $targetClassNames = $association->getAcceptableTargetClassNames(); + foreach ($targetClassNames as $className) { + if (isset($accessibleResources[$className])) { + return true; + } + } + + return false; + } + + /** + * @param string $entityClass + * @param string $version + * @param RequestType $requestType + * + * @return EntityDefinitionConfig|null + */ + protected function getConfig($entityClass, $version, RequestType $requestType) + { + return $this->configProvider + ->getConfig($entityClass, $version, $requestType, $this->getConfigExtras()) + ->getDefinition(); + } + + /** + * @param string $entityClass + * @param string $version + * @param RequestType $requestType + * @param EntityDefinitionConfig|null $config + * + * @return EntityMetadata|null + */ + protected function getMetadata( + $entityClass, + $version, + RequestType $requestType, + EntityDefinitionConfig $config = null + ) { + return $this->metadataProvider->getMetadata( + $entityClass, + $version, + $requestType, + $config, + $this->getMetadataExtras(), + true + ); + } + + /** + * @return ConfigExtraInterface[] + */ + protected function getConfigExtras() + { + return [new EntityDefinitionConfigExtra()]; + } + + /** + * @return MetadataExtraInterface[] + */ + protected function getMetadataExtras() + { + return []; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddAssociationValidators.php b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddAssociationValidators.php index 58dddf58d33..854f975c4a3 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddAssociationValidators.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddAssociationValidators.php @@ -51,7 +51,7 @@ protected function addValidatorsForEntityAssociations(EntityDefinitionConfig $de $metadata = $this->doctrineHelper->getEntityMetadataForClass($entityClass); $fields = $definition->getFields(); foreach ($fields as $fieldName => $field) { - $fieldName = $field->getPropertyPath() ?: $fieldName; + $fieldName = $field->getPropertyPath($fieldName); if ($metadata->hasAssociation($fieldName)) { $fieldOptions = $field->getFormOptions(); if ($metadata->isCollectionValuedAssociation($fieldName)) { @@ -78,7 +78,7 @@ protected function addValidatorsForObjectAssociations(EntityDefinitionConfig $de if ($field->getTargetClass() && $field->isCollectionValuedAssociation()) { $fieldOptions = $field->getFormOptions(); $fieldOptions['constraints'][] = new Assert\HasAdderAndRemover( - ['class' => $entityClass, 'property' => $field->getPropertyPath() ?: $fieldName] + ['class' => $entityClass, 'property' => $field->getPropertyPath($fieldName)] ); $field->setFormOptions($fieldOptions); } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddOwnerValidator.php b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddOwnerValidator.php index 1386bb6c943..db9c4655c1d 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddOwnerValidator.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/AddOwnerValidator.php @@ -84,7 +84,7 @@ protected function addValidators(EntityDefinitionConfig $definition, $entityClas } // add NotBlank constraint - $property = $field->getPropertyPath() ?: $fieldName; + $property = $field->getPropertyPath($fieldName); if (!$this->validationHelper->hasValidationConstraintForProperty($entityClass, $property, NotBlank::class)) { $field->addFormConstraint(new NotBlank()); } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataCustomizationHandler.php b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataCustomizationHandler.php index 23700433710..b4a6795f67e 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataCustomizationHandler.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataCustomizationHandler.php @@ -9,7 +9,7 @@ use Oro\Component\ChainProcessor\ProcessorInterface; use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Config\EntityDefinitionFieldConfig; -use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedDataContext; +use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedData\CustomizeLoadedDataContext; use Oro\Bundle\ApiBundle\Processor\Config\ConfigContext; use Oro\Bundle\ApiBundle\Util\ConfigUtil; use Oro\Bundle\ApiBundle\Util\DoctrineHelper; @@ -98,7 +98,7 @@ protected function processFields( ) { $fields = $definition->getFields(); foreach ($fields as $fieldName => $field) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if ($metadata->hasAssociation($propertyPath)) { $linkedMetadata = $this->doctrineHelper->getEntityMetadataForClass( $metadata->getAssociationTargetClass($propertyPath) diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataTransformers.php b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataTransformers.php index fafa0b728a2..023352f948b 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataTransformers.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetDataTransformers.php @@ -69,7 +69,7 @@ protected function setDataTransformers(EntityDefinitionConfig $definition, Class $targetConfig = $field->getTargetEntity(); if (null !== $targetConfig) { if (null !== $metadata && $targetConfig->hasFields()) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if ($metadata->hasAssociation($propertyPath)) { $this->setDataTransformers( $targetConfig, @@ -82,7 +82,7 @@ protected function setDataTransformers(EntityDefinitionConfig $definition, Class } else { $dataType = $field->getDataType(); if (null !== $metadata && !$dataType) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if ($metadata->hasField($propertyPath)) { $dataType = $metadata->getTypeOfField($propertyPath); } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetMaxRelatedEntities.php b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetMaxRelatedEntities.php index 9193aeb0a26..7fde9aaa345 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetMaxRelatedEntities.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/GetConfig/SetMaxRelatedEntities.php @@ -9,6 +9,7 @@ use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Config\EntityDefinitionFieldConfig; use Oro\Bundle\ApiBundle\Processor\Config\ConfigContext; +use Oro\Bundle\ApiBundle\Request\DataType; use Oro\Bundle\ApiBundle\Util\DoctrineHelper; /** @@ -67,7 +68,7 @@ protected function setEntityLimits(EntityDefinitionConfig $definition, ClassMeta { $fields = $definition->getFields(); foreach ($fields as $fieldName => $field) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if ($metadata->hasAssociation($propertyPath)) { $this->setEntityFieldLimit($field, $metadata, $propertyPath, $limit); } @@ -89,7 +90,9 @@ protected function setEntityFieldLimit( if ($metadata->isCollectionValuedAssociation($fieldName)) { $targetEntity = $field->getOrCreateTargetEntity(); if (!$targetEntity->hasMaxResults()) { - $targetEntity->setMaxResults($limit); + if (!DataType::isAssociationAsField($field->getDataType())) { + $targetEntity->setMaxResults($limit); + } } elseif ($targetEntity->getMaxResults() < 0) { $targetEntity->setMaxResults(null); } @@ -125,7 +128,9 @@ protected function setObjectFieldLimit(EntityDefinitionFieldConfig $field, $limi if ($field->isCollectionValuedAssociation()) { $targetEntity = $field->getOrCreateTargetEntity(); if (!$targetEntity->hasMaxResults()) { - $targetEntity->setMaxResults($limit); + if (!DataType::isAssociationAsField($field->getDataType())) { + $targetEntity->setMaxResults($limit); + } } elseif ($targetEntity->getMaxResults() < 0) { $targetEntity->setMaxResults(null); } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDefinition.php b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDefinition.php index 1a98f68c159..d4a18fac8f9 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDefinition.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDefinition.php @@ -131,7 +131,10 @@ protected function getExistingFields(EntityDefinitionConfig $definition) $existingFields = []; $fields = $definition->getFields(); foreach ($fields as $fieldName => $field) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath(); + if (empty($propertyPath) || ConfigUtil::IGNORE_PROPERTY_PATH === $propertyPath) { + $propertyPath = $fieldName; + } $existingFields[$propertyPath] = $fieldName; } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDescriptions.php b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDescriptions.php index c0c2979609f..f4a9c37863d 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDescriptions.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/CompleteDescriptions.php @@ -218,7 +218,7 @@ protected function setDescriptionsForFields(EntityDefinitionConfig $definition, $fields = $definition->getFields(); foreach ($fields as $fieldName => $field) { if (!$field->hasDescription()) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if ($fieldPrefix) { $propertyPath = $fieldPrefix . $propertyPath; } @@ -238,7 +238,7 @@ protected function setDescriptionsForFields(EntityDefinitionConfig $definition, if ($targetClass) { $this->setDescriptionsForFields($targetEntity, $targetClass); } else { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); $this->setDescriptionsForFields($targetEntity, $entityClass, $propertyPath . '.'); } } @@ -254,7 +254,7 @@ protected function setDescriptionsForFilters(FiltersConfig $filters, $entityClas $fields = $filters->getFields(); foreach ($fields as $fieldName => $field) { if (!$field->hasDescription()) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); $description = $this->entityDescriptionProvider->getFieldDescription($entityClass, $propertyPath); if ($description) { $field->setDescription($description); diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/ExcludeNotAccessibleRelations.php b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/ExcludeNotAccessibleRelations.php index cbb90f2133d..4c162de387a 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/ExcludeNotAccessibleRelations.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/ExcludeNotAccessibleRelations.php @@ -7,8 +7,10 @@ use Oro\Component\ChainProcessor\ContextInterface; use Oro\Component\ChainProcessor\ProcessorInterface; use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; +use Oro\Bundle\ApiBundle\Config\EntityDefinitionFieldConfig; use Oro\Bundle\ApiBundle\Processor\Config\ConfigContext; use Oro\Bundle\ApiBundle\Provider\ResourcesProvider; +use Oro\Bundle\ApiBundle\Request\DataType; use Oro\Bundle\ApiBundle\Request\RequestType; use Oro\Bundle\ApiBundle\Util\DoctrineHelper; @@ -77,19 +79,65 @@ protected function updateRelations( continue; } - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if (!$metadata->hasAssociation($propertyPath)) { continue; } $mapping = $metadata->getAssociationMapping($propertyPath); $targetMetadata = $this->doctrineHelper->getEntityMetadataForClass($mapping['targetEntity']); - if (!$this->isResourceForRelatedEntityAccessible($targetMetadata, $version, $requestType)) { + if (!$this->isResourceForRelatedEntityAvailable($field, $targetMetadata, $version, $requestType)) { $field->setExcluded(); } } } + /** + * @param EntityDefinitionFieldConfig $field + * @param ClassMetadata $targetMetadata + * @param string $version + * @param RequestType $requestType + * + * @return bool + */ + protected function isResourceForRelatedEntityAvailable( + EntityDefinitionFieldConfig $field, + ClassMetadata $targetMetadata, + $version, + RequestType $requestType + ) { + return DataType::isAssociationAsField($field->getDataType()) + ? $this->isResourceForRelatedEntityKnown($targetMetadata, $version, $requestType) + : $this->isResourceForRelatedEntityAccessible($targetMetadata, $version, $requestType); + } + + /** + * @param ClassMetadata $targetMetadata + * @param string $version + * @param RequestType $requestType + * + * @return bool + */ + protected function isResourceForRelatedEntityKnown( + ClassMetadata $targetMetadata, + $version, + RequestType $requestType + ) { + if ($this->resourcesProvider->isResourceKnown($targetMetadata->name, $version, $requestType)) { + return true; + } + if ($targetMetadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + // check that at least one inherited entity has Data API resource + foreach ($targetMetadata->subClasses as $inheritedEntityClass) { + if ($this->resourcesProvider->isResourceKnown($inheritedEntityClass, $version, $requestType)) { + return true; + } + } + } + + return false; + } + /** * @param ClassMetadata $targetMetadata * @param string $version diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/FilterFieldsByExtra.php b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/FilterFieldsByExtra.php index 0ddbc604041..112bfae3765 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/FilterFieldsByExtra.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/FilterFieldsByExtra.php @@ -112,7 +112,7 @@ protected function filterEntityFields( if (!$field->isExcluded() && !in_array($fieldName, $allowedFields, true) && !in_array($fieldName, $idFieldNames, true) - && !ConfigUtil::isMetadataProperty($field->getPropertyPath() ?: $fieldName) + && !ConfigUtil::isMetadataProperty($field->getPropertyPath($fieldName)) ) { $field->setExcluded(); } @@ -122,7 +122,7 @@ protected function filterEntityFields( $fields = $definition->getFields(); foreach ($fields as $fieldName => $field) { if ($field->hasTargetEntity()) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if ($metadata->hasAssociation($propertyPath)) { $this->filterEntityFields( $field->getTargetEntity(), diff --git a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/NormalizeSection.php b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/NormalizeSection.php index 8e74bd986ff..f74a5062482 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/NormalizeSection.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Config/Shared/NormalizeSection.php @@ -91,7 +91,7 @@ protected function collect( $entityClass, $targetEntity, $this->buildPrefix($fieldName), - $this->buildPrefix($field->getPropertyPath() ?: $fieldName) + $this->buildPrefix($field->getPropertyPath($fieldName)) ); $targetEntity->remove($sectionName); } diff --git a/src/Oro/Bundle/ApiBundle/Processor/ContextConfigAccessor.php b/src/Oro/Bundle/ApiBundle/Processor/ContextConfigAccessor.php new file mode 100644 index 00000000000..ed407ff640f --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/ContextConfigAccessor.php @@ -0,0 +1,29 @@ +context = $context; + } + + /** + * {@inheritdoc} + */ + public function getConfig($className) + { + return $this->context->getClassName() === $className + ? $this->context->getConfig() + : null; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/Create/SaveEntity.php b/src/Oro/Bundle/ApiBundle/Processor/Create/SaveEntity.php index 136ba52ccff..17c1428c759 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Create/SaveEntity.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Create/SaveEntity.php @@ -45,7 +45,7 @@ public function process(ContextInterface $context) } $em->persist($entity); - $em->flush($entity); + $em->flush(); // save entity id into the Context $metadata = $em->getClassMetadata(ClassUtils::getClass($entity)); diff --git a/src/Oro/Bundle/ApiBundle/Processor/CustomizeDataContext.php b/src/Oro/Bundle/ApiBundle/Processor/CustomizeDataContext.php new file mode 100644 index 00000000000..09ff9e95c12 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CustomizeDataContext.php @@ -0,0 +1,130 @@ +get(self::ROOT_CLASS_NAME); + } + + /** + * Sets FQCN of a root entity. + * + * @param string $className + */ + public function setRootClassName($className) + { + $this->set(self::ROOT_CLASS_NAME, $className); + } + + /** + * Gets a path inside a root entity to a customizing entity. + * + * @return string|null + */ + public function getPropertyPath() + { + return $this->get(self::PROPERTY_PATH); + } + + /** + * Sets a path inside a root entity to a customizing entity. + * + * @param string $propertyPath + */ + public function setPropertyPath($propertyPath) + { + $this->set(self::PROPERTY_PATH, $propertyPath); + } + + /** + * Gets FQCN of a customizing entity. + * + * @return string + */ + public function getClassName() + { + return $this->get(self::CLASS_NAME); + } + + /** + * Sets FQCN of a customizing entity. + * + * @param string $className + */ + public function setClassName($className) + { + $this->set(self::CLASS_NAME, $className); + } + + /** + * Gets a configuration of a root entity. + * + * @return EntityDefinitionConfig|null + */ + public function getRootConfig() + { + return $this->getPropertyPath() + ? $this->config + : null; + } + + /** + * Gets a configuration of a customizing entity. + * + * @return EntityDefinitionConfig|null + */ + public function getConfig() + { + $config = $this->config; + if (null !== $config) { + $propertyPath = $this->getPropertyPath(); + if ($propertyPath) { + $path = ConfigUtil::explodePropertyPath($propertyPath); + foreach ($path as $fieldName) { + $fieldConfig = $config->getField($fieldName); + $config = null !== $fieldConfig + ? $fieldConfig->getTargetEntity() + : null; + if (null === $config) { + break; + } + } + } + } + + return $config; + } + + /** + * Sets a configuration of a customizing entity. + * + * @param EntityDefinitionConfig|null $config + */ + public function setConfig(EntityDefinitionConfig $config = null) + { + $this->config = $config; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/AbstractProcessor.php b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/AbstractProcessor.php new file mode 100644 index 00000000000..af95e95d8e1 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/AbstractProcessor.php @@ -0,0 +1,60 @@ +getEvent()) { + case CustomizeFormDataContext::EVENT_PRE_SUBMIT: + $this->processPreSubmit($context); + break; + case CustomizeFormDataContext::EVENT_SUBMIT: + $this->processSubmit($context); + break; + case CustomizeFormDataContext::EVENT_POST_SUBMIT: + $this->processPostSubmit($context); + break; + case CustomizeFormDataContext::EVENT_FINISH_SUBMIT: + $this->processFinishSubmit($context); + break; + } + } + + /** + * @param CustomizeFormDataContext $context + */ + protected function processPreSubmit(CustomizeFormDataContext $context) + { + } + + /** + * @param CustomizeFormDataContext $context + */ + protected function processSubmit(CustomizeFormDataContext $context) + { + } + + /** + * @param CustomizeFormDataContext $context + */ + protected function processPostSubmit(CustomizeFormDataContext $context) + { + } + + /** + * @param CustomizeFormDataContext $context + */ + protected function processFinishSubmit(CustomizeFormDataContext $context) + { + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/CustomizeFormDataContext.php b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/CustomizeFormDataContext.php new file mode 100644 index 00000000000..4de3b328505 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/CustomizeFormDataContext.php @@ -0,0 +1,144 @@ +get(self::EVENT); + } + + /** + * Gets the form event name. + * + * @param string $event One of "pre_submit", "submit", "post_submit" and "finish_submit" + */ + public function setEvent($event) + { + $this->set(self::EVENT, $event); + } + + /** + * Gets a form object related to a customizing entity. + * + * @return FormInterface + */ + public function getForm() + { + return $this->form; + } + + /** + * Sets a form object related to a customizing entity. + * + * @param FormInterface $form + */ + public function setForm(FormInterface $form) + { + $this->form = $form; + } + + /** + * Gets the data associated with form event event. + * For "pre_submit" event it is the submitted data. + * For "submit" event it is the norm data. + * For "post_submit" and "finish_submit" events it is the view data. + * + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * Sets the data associated with form event event. + * + * @param mixed $data + */ + public function setData($data) + { + $this->data = $data; + } + + /** + * This method is just an alias for getData. + * + * @return mixed + */ + public function getResult() + { + return $this->data; + } + + /** + * This method is just an alias for setData. + * + * @param mixed $data + */ + public function setResult($data) + { + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function hasResult() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function removeResult() + { + throw new \BadMethodCallException('Not implemented.'); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/MapPrimaryField.php b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/MapPrimaryField.php new file mode 100644 index 00000000000..8fa0305786a --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormData/MapPrimaryField.php @@ -0,0 +1,270 @@ +propertyAccessor = $propertyAccessor; + $this->unknownPrimaryValueValidationMessage = $unknownPrimaryValueValidationMessage; + $this->primaryFieldName = $primaryFieldName; + $this->associationName = $associationName; + $this->associationDataFieldName = $associationDataFieldName; + $this->associationPrimaryFlagFieldName = $associationPrimaryFlagFieldName; + } + + /** + * {@inheritdoc} + */ + public function processPreSubmit(CustomizeFormDataContext $context) + { + $submittedData = $context->getData(); + if (!is_array($submittedData) && !$submittedData instanceof \ArrayAccess) { + return; + } + $form = $context->getForm(); + if (array_key_exists($this->associationName, $submittedData) || !$form->has($this->associationName)) { + return; + } + if (empty($submittedData[$this->primaryFieldName]) || !$form->has($this->primaryFieldName)) { + return; + } + $associationField = $context->getConfig()->getField($this->associationName); + if (null === $associationField) { + return; + } + + list($collectionSubmitData, $primaryItemKey) = $this->getAssociationSubmitData( + $form->get($this->associationName)->getData(), + $submittedData[$this->primaryFieldName], + $associationField + ); + $submittedData[$this->associationName] = $collectionSubmitData; + $context->setData($submittedData); + if (null !== $primaryItemKey) { + $context->set(self::PRIMARY_ITEM_KEY, $primaryItemKey); + } + } + + /** + * {@inheritdoc} + */ + public function processPostSubmit(CustomizeFormDataContext $context) + { + $primaryFieldForm = $context->getForm()->get($this->primaryFieldName); + if (!$primaryFieldForm->isSubmitted()) { + // the primary field does not exist in the submitted data + return; + } + + $primaryField = $context->getConfig()->getField($this->primaryFieldName); + if (null === $primaryField) { + return; + } + $associationField = $context->getConfig()->getField($this->associationName); + if (null === $associationField) { + return; + } + + $data = $context->getData(); + $primaryValue = $primaryFieldForm->getViewData(); + $collection = $this->propertyAccessor->getValue( + $data, + $associationField->getPropertyPath($this->associationName) + ); + $isKnownPrimaryValue = $this->updateAssociationData( + $collection, + $this->getAssociationFieldPropertyPath($associationField, $this->associationDataFieldName), + $this->getAssociationFieldPropertyPath($associationField, $this->associationPrimaryFlagFieldName), + $primaryValue + ); + + if ($primaryValue && !$isKnownPrimaryValue) { + $primaryFieldForm->addError( + new FormError($this->unknownPrimaryValueValidationMessage) + ); + } + } + + /** + * {@inheritdoc} + */ + public function processFinishSubmit(CustomizeFormDataContext $context) + { + $form = $context->getForm(); + $primaryItemKey = $context->get(self::PRIMARY_ITEM_KEY); + if (null !== $primaryItemKey && $form->has($this->associationName)) { + $associationForm = $form->get($this->associationName); + if ($associationForm->has($primaryItemKey)) { + ReflectionUtil::clearFormErrors($associationForm->get($primaryItemKey), true); + } + } + } + + /** + * @param mixed $collection + * @param string $primaryValue + * @param EntityDefinitionFieldConfig $association + * + * @return array [collection submit data, the primary item key] + */ + protected function getAssociationSubmitData( + $collection, + $primaryValue, + EntityDefinitionFieldConfig $association + ) { + $this->assertAssociationData($collection); + + $isCollapsed = $association->isCollapsed(); + $dataPropertyPath = $this->getAssociationFieldPropertyPath( + $association, + $this->associationDataFieldName + ); + + $result = []; + $hasPrimaryItem = false; + foreach ($collection as $item) { + $value = $this->propertyAccessor->getValue($item, $dataPropertyPath); + if (trim($value) === trim($primaryValue)) { + $hasPrimaryItem = true; + } + $result[] = $this->getAssociationSubmittedDataItem($value, $isCollapsed); + } + $primaryItemKey = null; + if (!$hasPrimaryItem) { + $primaryItemKey = (string)count($result); + $result[] = $this->getAssociationSubmittedDataItem($primaryValue, $isCollapsed); + } + + return [$result, $primaryItemKey]; + } + + /** + * @param mixed $value + * @param bool $isCollapsed + * + * @return mixed + */ + protected function getAssociationSubmittedDataItem($value, $isCollapsed) + { + return $isCollapsed + ? $value + : [$this->associationDataFieldName => $value]; + } + + /** + * @param mixed $collection + * @param string $dataPropertyPath + * @param string $primaryFlagPropertyPath + * @param mixed $primaryValue + * + * @return bool + */ + protected function updateAssociationData( + $collection, + $dataPropertyPath, + $primaryFlagPropertyPath, + $primaryValue + ) { + $this->assertAssociationData($collection); + + $isKnownPrimaryValue = false; + foreach ($collection as $item) { + $value = $this->propertyAccessor->getValue($item, $dataPropertyPath); + $primaryFlag = $this->propertyAccessor->getValue($item, $primaryFlagPropertyPath); + if ($primaryValue && $primaryValue == $value) { + $isKnownPrimaryValue = true; + if (!$primaryFlag) { + $this->propertyAccessor->setValue($item, $primaryFlagPropertyPath, true); + } + } elseif ($primaryFlag) { + $this->propertyAccessor->setValue($item, $primaryFlagPropertyPath, false); + } + } + + return $isKnownPrimaryValue; + } + + /** + * @param EntityDefinitionFieldConfig $association + * @param string $fieldName + * + * @return string + */ + protected function getAssociationFieldPropertyPath(EntityDefinitionFieldConfig $association, $fieldName) + { + $result = $fieldName; + $associationTarget = $association->getTargetEntity(); + if (null !== $associationTarget) { + $field = $associationTarget->getField($fieldName); + if (null !== $field) { + $propertyPath = $field->getPropertyPath(); + if ($propertyPath) { + $result = $propertyPath; + } + } + } + + return $result; + } + + /** + * @param mixed $collection + */ + protected function assertAssociationData($collection) + { + if (!$collection instanceof \Traversable && !is_array($collection)) { + throw new \RuntimeException( + sprintf( + 'The "%s" field should be \Traversable or array, got "%s".', + $this->associationName, + is_object($collection) ? get_class($collection) : gettype($collection) + ) + ); + } + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormDataProcessor.php b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormDataProcessor.php new file mode 100644 index 00000000000..28d60d3c14a --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CustomizeFormDataProcessor.php @@ -0,0 +1,17 @@ +primaryFieldName = $primaryFieldName; + $this->associationName = $associationName; + $this->associationDataFieldName = $associationDataFieldName; + $this->associationPrimaryFlagFieldName = $associationPrimaryFlagFieldName; + } + + /** + * {@inheritdoc} + */ + public function process(ContextInterface $context) + { + /** @var CustomizeLoadedDataContext $context */ + + $data = $context->getResult(); + if (!is_array($data)) { + return; + } + + $config = $context->getConfig(); + $primaryField = $config->getField($this->primaryFieldName); + if (!$primaryField || $primaryField->isExcluded()) { + // undefined or excluded primary field + return; + } + if (array_key_exists($this->primaryFieldName, $data)) { + // the primary field is already set + return; + } + + $data[$this->primaryFieldName] = $this->getPrimaryValue($config, $data); + $context->setResult($data); + } + + /** + * @param EntityDefinitionConfig $config + * @param array $data + * + * @return mixed + */ + protected function getPrimaryValue(EntityDefinitionConfig $config, array $data) + { + $result = null; + $association = $config->getField($this->associationName); + if (null !== $association) { + $associationName = $association->getPropertyPath($this->associationName); + if (!empty($data[$associationName]) && is_array($data[$associationName])) { + $associationTargetConfig = $association->getTargetEntity(); + if (null !== $associationTargetConfig) { + $result = $this->extractPrimaryValue( + $data[$associationName], + $this->getPropertyPath($associationTargetConfig, $this->associationDataFieldName), + $this->getPropertyPath($associationTargetConfig, $this->associationPrimaryFlagFieldName) + ); + } + } + } + + return $result; + } + + /** + * @param array $items + * @param string $dataFieldName + * @param string $primaryFlagFieldName + * + * @return mixed + */ + protected function extractPrimaryValue(array $items, $dataFieldName, $primaryFlagFieldName) + { + $result = null; + foreach ($items as $item) { + if (is_array($item) + && array_key_exists($primaryFlagFieldName, $item) + && $item[$primaryFlagFieldName] + && array_key_exists($dataFieldName, $item) + ) { + $result = $item[$dataFieldName]; + break; + } + } + + return $result; + } + + /** + * @param EntityDefinitionConfig $config + * @param string $fieldName + * + * @return string + */ + protected function getPropertyPath(EntityDefinitionConfig $config, $fieldName) + { + $field = $config->getField($fieldName); + if (null === $field) { + return $fieldName; + } + + return $field->getPropertyPath($fieldName); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/CustomizeLoadedData/CustomizeLoadedDataContext.php b/src/Oro/Bundle/ApiBundle/Processor/CustomizeLoadedData/CustomizeLoadedDataContext.php new file mode 100644 index 00000000000..d232b22627f --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/CustomizeLoadedData/CustomizeLoadedDataContext.php @@ -0,0 +1,9 @@ +get(self::ROOT_CLASS_NAME); - } - - /** - * Sets FQCN of a root entity. - * - * @param string $className - */ - public function setRootClassName($className) - { - $this->set(self::ROOT_CLASS_NAME, $className); - } - - /** - * Gets a path inside a root entity to a customizing entity. - * - * @return string|null - */ - public function getPropertyPath() - { - return $this->get(self::PROPERTY_PATH); - } - - /** - * Sets a path inside a root entity to a customizing entity. - * - * @param string $propertyPath - */ - public function setPropertyPath($propertyPath) - { - $this->set(self::PROPERTY_PATH, $propertyPath); - } - - /** - * Gets FQCN of a customizing entity. - * - * @return string - */ - public function getClassName() - { - return $this->get(self::CLASS_NAME); - } - - /** - * Sets FQCN of a customizing entity. - * - * @param string $className - */ - public function setClassName($className) - { - $this->set(self::CLASS_NAME, $className); - } - - /** - * Gets a configuration of a root entity. - * - * @return EntityDefinitionConfig|null - */ - public function getRootConfig() - { - return $this->getPropertyPath() - ? $this->config - : null; - } - - /** - * Gets a configuration of a customizing entity. - * - * @return EntityDefinitionConfig|null - */ - public function getConfig() - { - $config = $this->config; - if (null !== $config) { - $propertyPath = $this->getPropertyPath(); - if ($propertyPath) { - $path = ConfigUtil::explodePropertyPath($propertyPath); - foreach ($path as $fieldName) { - $fieldConfig = $config->getField($fieldName); - $config = null !== $fieldConfig - ? $fieldConfig->getTargetEntity() - : null; - if (null === $config) { - break; - } - } - } - } - - return $config; - } - - /** - * Sets a configuration of a customizing entity. - * - * @param EntityDefinitionConfig|null $config - */ - public function setConfig(EntityDefinitionConfig $config = null) - { - $this->config = $config; - } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/LoadMetadata.php b/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/LoadMetadata.php index bd1c74f4c7a..2bd8950e7f7 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/LoadMetadata.php +++ b/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/LoadMetadata.php @@ -74,11 +74,11 @@ public function process(ContextInterface $context) $entityClass = $context->getClassName(); $config = $context->getConfig(); if ($this->doctrineHelper->isManageableEntityClass($entityClass)) { - $entityMetadata = $this->loadEntityMetadata($entityClass, $config); + $entityMetadata = $this->loadEntityMetadata($entityClass, $config, $context->getWithExcludedProperties()); $this->completeAssociationMetadata($entityMetadata, $config, $context); $context->setResult($entityMetadata); } elseif ($config->hasFields()) { - $entityMetadata = $this->loadObjectMetadata($entityClass, $config); + $entityMetadata = $this->loadObjectMetadata($entityClass, $config, $context->getWithExcludedProperties()); $this->completeAssociationMetadata($entityMetadata, $config, $context); $context->setResult($entityMetadata); } @@ -87,20 +87,21 @@ public function process(ContextInterface $context) /** * @param string $entityClass * @param EntityDefinitionConfig $config + * @param bool $withExcludedProperties * * @return EntityMetadata */ - protected function loadEntityMetadata($entityClass, EntityDefinitionConfig $config) + protected function loadEntityMetadata($entityClass, EntityDefinitionConfig $config, $withExcludedProperties) { // filter excluded fields on this stage though there is another processor doing the same // it is done due to performance reasons - $allowedFields = $this->getAllowedFields($config); + $allowedFields = $this->getAllowedFields($config, $withExcludedProperties); $classMetadata = $this->doctrineHelper->getEntityMetadataForClass($entityClass); $entityMetadata = $this->createEntityMetadata($classMetadata, $config); $this->loadFields($entityMetadata, $classMetadata, $allowedFields, $config); $this->loadAssociations($entityMetadata, $classMetadata, $allowedFields, $config); - $this->loadPropertiesFromConfig($entityMetadata, $config); + $this->loadPropertiesFromConfig($entityMetadata, $config, $withExcludedProperties); return $entityMetadata; } @@ -199,6 +200,7 @@ protected function loadAssociations( if ($propertyPath !== $associationName) { $associationMetadata->setName($associationName); } + $associationMetadata->setCollapsed($field->isCollapsed()); $entityMetadata->addAssociation($associationMetadata); } } @@ -206,15 +208,17 @@ protected function loadAssociations( /** * @param EntityMetadata $entityMetadata * @param EntityDefinitionConfig $config + * @param bool $withExcludedProperties */ protected function loadPropertiesFromConfig( EntityMetadata $entityMetadata, - EntityDefinitionConfig $config + EntityDefinitionConfig $config, + $withExcludedProperties ) { $entityClass = $entityMetadata->getClassName(); $fields = $config->getFields(); foreach ($fields as $fieldName => $field) { - if ($field->isExcluded()) { + if (!$withExcludedProperties && $field->isExcluded()) { continue; } if (!$field->isMetaProperty()) { @@ -290,6 +294,7 @@ protected function addAssociationMetadata( $associationMetadata = $entityMetadata->addAssociation(new AssociationMetadata($fieldName)); $associationMetadata->setTargetClassName($targetClass); $associationMetadata->setIsNullable(true); + $associationMetadata->setCollapsed($field->isCollapsed()); if (0 !== strpos($dataType, 'association:')) { $associationMetadata->setDataType($dataType); $this->setAssociationType($associationMetadata, $field->isCollectionValuedAssociation()); @@ -352,16 +357,17 @@ protected function getExtendedAssociationTargets($entityClass, $associationType, /** * @param EntityDefinitionConfig $config + * @param bool $withExcludedProperties * * @return array [property path => field name, ...] */ - protected function getAllowedFields(EntityDefinitionConfig $config) + protected function getAllowedFields(EntityDefinitionConfig $config, $withExcludedProperties) { $result = []; $fields = $config->getFields(); foreach ($fields as $fieldName => $field) { - if (!$field->isExcluded()) { - $propertyPath = $field->getPropertyPath() ?: $fieldName; + if ($withExcludedProperties || !$field->isExcluded()) { + $propertyPath = $field->getPropertyPath($fieldName); $result[$propertyPath] = $fieldName; } } @@ -372,10 +378,11 @@ protected function getAllowedFields(EntityDefinitionConfig $config) /** * @param string $entityClass * @param EntityDefinitionConfig $config + * @param bool $withExcludedProperties * * @return EntityMetadata */ - protected function loadObjectMetadata($entityClass, EntityDefinitionConfig $config) + protected function loadObjectMetadata($entityClass, EntityDefinitionConfig $config, $withExcludedProperties) { $entityMetadata = new EntityMetadata(); $entityMetadata->setClassName($entityClass); @@ -383,7 +390,7 @@ protected function loadObjectMetadata($entityClass, EntityDefinitionConfig $conf $entityMetadata->setIdentifierFieldNames($idFieldNames); $fields = $config->getFields(); foreach ($fields as $fieldName => $field) { - if ($field->isExcluded()) { + if (!$withExcludedProperties && $field->isExcluded()) { continue; } $targetClass = $field->getTargetClass(); @@ -391,9 +398,12 @@ protected function loadObjectMetadata($entityClass, EntityDefinitionConfig $conf $associationMetadata = $entityMetadata->addAssociation(new AssociationMetadata($fieldName)); if (!$field->getDataType()) { $this->setAssociationDataType($associationMetadata, $field); + } else { + $associationMetadata->setDataType($field->getDataType()); } $this->setAssociationType($associationMetadata, $field->isCollectionValuedAssociation()); $associationMetadata->setIsNullable(true); + $associationMetadata->setCollapsed($field->isCollapsed()); $associationMetadata->setTargetClassName($targetClass); $associationMetadata->addAcceptableTargetClassName($targetClass); } elseif ($field->isMetaProperty()) { diff --git a/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/MetadataContext.php b/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/MetadataContext.php index d8fee4257d3..8d8658908d6 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/MetadataContext.php +++ b/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/MetadataContext.php @@ -18,6 +18,9 @@ class MetadataContext extends ApiContext /** a list of requests for additional metadata information that should be retrieved */ const EXTRA = 'extra'; + /** whether excluded fields and associations should not be removed */ + const WITH_EXCLUDED_PROPERTIES = 'withExcludedProperties'; + /** @var MetadataExtraInterface[] */ protected $extras = []; @@ -118,4 +121,24 @@ public function setExtras(array $extras) $this->extras = $extras; $this->set(self::EXTRA, $names); } + + /** + * Gets a flag indicates whether excluded fields and associations should not be removed. + * + * @return bool + */ + public function getWithExcludedProperties() + { + return (bool)$this->get(self::WITH_EXCLUDED_PROPERTIES); + } + + /** + * Sets a flag indicates whether excluded fields and associations should not be removed. + * + * @param bool $flag + */ + public function setWithExcludedProperties($flag) + { + $this->set(self::WITH_EXCLUDED_PROPERTIES, $flag); + } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/NormalizeMetadata.php b/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/NormalizeMetadata.php index 86388225d0c..8e19cc64ff5 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/NormalizeMetadata.php +++ b/src/Oro/Bundle/ApiBundle/Processor/GetMetadata/NormalizeMetadata.php @@ -87,9 +87,10 @@ protected function normalizeMetadata( MetadataContext $context ) { $linkedPropertyNames = []; + $withExcludedProperties = $context->getWithExcludedProperties(); $fields = $config->getFields(); foreach ($fields as $fieldName => $field) { - if ($field->isExcluded()) { + if (!$withExcludedProperties && $field->isExcluded()) { $entityMetadata->removeProperty($fieldName); } else { $propertyPath = $field->getPropertyPath(); @@ -158,13 +159,21 @@ protected function processLinkedProperty( $linkedProperty ); $associationMetadata->setName($propertyName); + $linkedPropertyPath = array_merge($propertyPath, [$linkedProperty]); $associationMetadata->setTargetMetadata( $this->getMetadata( $associationMetadata->getTargetClassName(), - $this->getTargetConfig($config, $propertyName, array_merge($propertyPath, [$linkedProperty])), + $this->getTargetConfig($config, $propertyName, $linkedPropertyPath), $context ) ); + $targetFieldConfig = $this->findFieldByPropertyPath($config, $linkedPropertyPath); + if (null !== $targetFieldConfig) { + $associationMetadata->setCollapsed($targetFieldConfig->isCollapsed()); + if ($targetFieldConfig->getDataType()) { + $associationMetadata->setDataType($targetFieldConfig->getDataType()); + } + } $entityMetadata->addAssociation($associationMetadata); } else { $fieldMetadata = $this->entityMetadataFactory->createFieldMetadata( diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildCriteria.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildCriteria.php index 3e8dc198f3b..e52eccd5533 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildCriteria.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildCriteria.php @@ -2,45 +2,16 @@ namespace Oro\Bundle\ApiBundle\Processor\Shared; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; - use Oro\Component\ChainProcessor\ContextInterface; use Oro\Component\ChainProcessor\ProcessorInterface; -use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfigExtra; -use Oro\Bundle\ApiBundle\Config\FiltersConfigExtra; -use Oro\Bundle\ApiBundle\Filter\ComparisonFilter; -use Oro\Bundle\ApiBundle\Filter\FilterValue; use Oro\Bundle\ApiBundle\Filter\FilterInterface; -use Oro\Bundle\ApiBundle\Model\Error; -use Oro\Bundle\ApiBundle\Model\ErrorSource; use Oro\Bundle\ApiBundle\Processor\Context; -use Oro\Bundle\ApiBundle\Provider\ConfigProvider; -use Oro\Bundle\ApiBundle\Request\Constraint; -use Oro\Bundle\ApiBundle\Util\DoctrineHelper; /** * Applies all requested filters to the Criteria object. */ class BuildCriteria implements ProcessorInterface { - /** @var ConfigProvider */ - protected $configProvider; - - /** @var DoctrineHelper */ - protected $doctrineHelper; - - /** - * @param ConfigProvider $configProvider - * @param DoctrineHelper $doctrineHelper - */ - public function __construct( - ConfigProvider $configProvider, - DoctrineHelper $doctrineHelper - ) { - $this->configProvider = $configProvider; - $this->doctrineHelper = $doctrineHelper; - } - /** * {@inheritdoc} */ @@ -59,135 +30,13 @@ public function process(ContextInterface $context) return; } - $filterValues = $context->getFilterValues(); - $processedFilterKeys = []; - $filters = $context->getFilters(); + $filterValues = $context->getFilterValues(); /** @var FilterInterface $filter */ foreach ($filters as $filterKey => $filter) { if ($filterValues->has($filterKey)) { $filter->apply($criteria, $filterValues->get($filterKey)); - - $processedFilterKeys[$filterKey] = true; - } - } - - // process unknown filters - $filterValues = $filterValues->getGroup('filter'); - foreach ($filterValues as $filterKey => $filterValue) { - if (isset($processedFilterKeys[$filterKey])) { - continue; - } - if ($filters->has($filterKey)) { - continue; - } - - $filter = $this->getFilter($filterValue, $context); - if ($filter) { - $filter->apply($criteria, $filterValue); - $filters->add($filterKey, $filter); - } else { - $context->addError( - Error::createValidationError( - Constraint::FILTER, - sprintf('Filter "%s" is not supported.', $filterKey) - )->setSource(ErrorSource::createByParameter($filterKey)) - ); - } - } - } - - /** - * @param FilterValue $filterValue - * @param Context $context - * - * @return ComparisonFilter|null - */ - protected function getFilter(FilterValue $filterValue, Context $context) - { - /** @var ClassMetadata $metadata */ - $metadata = $this->doctrineHelper->getEntityMetadataForClass($context->getClassName(), false); - if (!$metadata) { - return null; - } - - $fieldName = $filterValue->getPath(); - $path = explode('.', $fieldName); - $associations = null; - if (count($path) > 1) { - $fieldName = array_pop($path); - list($filtersConfig, $associations) = $this->getAssociationFilters($path, $context, $metadata); - } else { - $filtersConfig = $context->getConfigOfFilters(); - } - if (!$filtersConfig) { - return null; - } - $filterConfig = $filtersConfig->getField($fieldName); - if (!$filterConfig) { - return null; - } - - $filterValueField = $filterConfig->getPropertyPath() ?: $fieldName; - if ($associations) { - $filterValueField = $associations . '.' . $filterValueField; - } - - $filter = new ComparisonFilter($filterConfig->getDataType()); - $filter->setField($filterValueField); - - return $filter; - } - - /** - * @param string[] $path - * @param Context $context - * @param ClassMetadata $metadata - * - * @return array [filters config, associations] - */ - protected function getAssociationFilters(array $path, Context $context, ClassMetadata $metadata) - { - $targetConfigExtras = [ - new EntityDefinitionConfigExtra($context->getAction()), - new FiltersConfigExtra() - ]; - - $config = $context->getConfig(); - $filters = null; - $associations = []; - - foreach ($path as $fieldName) { - if (!$config->hasField($fieldName)) { - return [null, null]; - } - - $associationName = $config->getField($fieldName)->getPropertyPath() ?: $fieldName; - if (!$metadata->hasAssociation($associationName)) { - return [null, null]; } - - $targetClass = $metadata->getAssociationTargetClass($associationName); - $metadata = $this->doctrineHelper->getEntityMetadataForClass($targetClass, false); - if (!$metadata) { - return [null, null]; - } - - $targetConfig = $this->configProvider->getConfig( - $targetClass, - $context->getVersion(), - $context->getRequestType(), - $targetConfigExtras - ); - if (!$targetConfig->hasDefinition()) { - return [null, null]; - } - - $config = $targetConfig->getDefinition(); - $filters = $targetConfig->getFilters(); - $associations[] = $associationName; } - - return [$filters, implode('.', $associations)]; } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildFormBuilder.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildFormBuilder.php index fe83dcf741b..93d64e36716 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildFormBuilder.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/BuildFormBuilder.php @@ -9,8 +9,10 @@ use Oro\Component\ChainProcessor\ProcessorInterface; use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Config\EntityDefinitionFieldConfig; +use Oro\Bundle\ApiBundle\Form\Extension\CustomizeFormDataExtension; use Oro\Bundle\ApiBundle\Metadata\EntityMetadata; use Oro\Bundle\ApiBundle\Processor\FormContext; +use Oro\Bundle\ApiBundle\Util\ConfigUtil; /** * Builds the form builder based on the entity metadata and configuration @@ -81,6 +83,7 @@ protected function getFormOptions(FormContext $context, EntityDefinitionConfig $ if (!array_key_exists('extra_fields_message', $options)) { $options['extra_fields_message'] = 'This form should not contain extra fields: "{{ extra_fields }}"'; } + $options[CustomizeFormDataExtension::API_CONTEXT] = $context; return $options; } @@ -129,7 +132,11 @@ protected function getFormFieldOptions(EntityDefinitionFieldConfig $fieldConfig) } $propertyPath = $fieldConfig->getPropertyPath(); if ($propertyPath) { - $options['property_path'] = $propertyPath; + if (ConfigUtil::IGNORE_PROPERTY_PATH === $propertyPath) { + $options['mapped'] = false; + } else { + $options['property_path'] = $propertyPath; + } } return $options; diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php index d32abfe6e61..2fc1393c559 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php @@ -125,7 +125,12 @@ protected function getFieldErrorPropertyPath(FormError $error, FormInterface $fi $cause = $error->getCause(); if ($cause instanceof ConstraintViolation) { - $result = $this->getConstraintViolationPropertyPath($cause); + $path = $this->getFormFieldPath($field); + $causePath = $this->getConstraintViolationPath($cause); + if (count($causePath) > count($path)) { + $path = $causePath; + } + $result = implode('.', $path); } if (!$result) { $result = $field->getName(); @@ -140,15 +145,45 @@ protected function getFieldErrorPropertyPath(FormError $error, FormInterface $fi * @return string|null */ protected function getConstraintViolationPropertyPath(ConstraintViolation $constraintViolation) + { + $path = $this->getConstraintViolationPath($constraintViolation); + + return !empty($path) + ? implode('.', $path) + : null; + } + + /** + * @param ConstraintViolation $constraintViolation + * + * @return string[] + */ + protected function getConstraintViolationPath(ConstraintViolation $constraintViolation) { $propertyPath = $constraintViolation->getPropertyPath(); if (!$propertyPath) { - return null; + return []; } $path = new ViolationPath($propertyPath); - return implode('.', $path->getElements()); + return $path->getElements(); + } + + /** + * @param FormInterface $field + * + * @return string[] + */ + protected function getFormFieldPath(FormInterface $field) + { + $path = []; + while (null !== $field->getParent()) { + $path[] = $field->getName(); + $field = $field->getParent(); + } + + return array_reverse($path); } /** diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/InitializeApiFormExtension.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/InitializeApiFormExtension.php index 956eb3c6f39..17b860d3c73 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/InitializeApiFormExtension.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/InitializeApiFormExtension.php @@ -7,6 +7,7 @@ use Oro\Bundle\ApiBundle\Form\FormExtensionSwitcherInterface; use Oro\Bundle\ApiBundle\Form\Guesser\MetadataTypeGuesser; use Oro\Bundle\ApiBundle\Processor\Context; +use Oro\Bundle\ApiBundle\Processor\ContextConfigAccessor; use Oro\Bundle\ApiBundle\Processor\ContextMetadataAccessor; /** @@ -41,5 +42,6 @@ public function process(ContextInterface $context) $this->formExtensionSwitcher->switchToApiFormExtension(); $this->metadataTypeGuesser->setMetadataAccessor(new ContextMetadataAccessor($context)); + $this->metadataTypeGuesser->setConfigAccessor(new ContextConfigAccessor($context)); } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterConfiguredFilters.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterConfiguredFilters.php new file mode 100644 index 00000000000..b672724fc11 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterConfiguredFilters.php @@ -0,0 +1,105 @@ +doctrineHelper = $doctrineHelper; + } + + /** + * {@inheritdoc} + */ + public function process(ContextInterface $context) + { + /** @var Context $context */ + + $configOfFilters = $context->getConfigOfFilters(); + if (null === $configOfFilters || $configOfFilters->isEmpty()) { + // a filters' configuration does not contains any data + return; + } + + if (!$configOfFilters->isExcludeAll()) { + // it seems that filters' configuration was not normalized + throw new RuntimeException( + sprintf( + 'Expected "all" exclusion policy for filters. Got: %s.', + $configOfFilters->getExclusionPolicy() + ) + ); + } + + /** @var ClassMetadata $metadata */ + $metadata = $this->doctrineHelper->getEntityMetadataForClass($context->getClassName(), false); + $filters = $context->getFilters(); + $fields = $configOfFilters->getFields(); + foreach ($fields as $fieldName => $field) { + if ($filters->has($fieldName)) { + continue; + } + $filter = $this->createFilter($field, $field->getPropertyPath($fieldName)); + if (null !== $filter) { + if ($filter instanceof FieldAwareFilterInterface) { + // @todo BAP-11881. Update this code when NEQ operator for to-many collection + // will be implemented in Oro\Bundle\ApiBundle\Filter\ComparisonFilter + if (null !== $metadata && $this->isCollection($metadata, $field->getPropertyPath($fieldName))) { + $filter->setSupportedOperators([StandaloneFilter::EQ]); + } + } + $filters->add($fieldName, $filter); + } + } + } + + /** + * @param ClassMetadata $metadata + * @param string $propertyPath + * + * @return bool + */ + protected function isCollection(ClassMetadata $metadata, $propertyPath) + { + $isCollection = false; + $path = explode('.', $propertyPath); + foreach ($path as $fieldName) { + if ($metadata->isCollectionValuedAssociation($fieldName)) { + $isCollection = true; + break; + } elseif (!$metadata->hasAssociation($fieldName)) { + break; + } + + $metadata = $this->doctrineHelper->getEntityMetadataForClass( + $metadata->getAssociationTargetClass($fieldName) + ); + } + + return $isCollection; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterDynamicFilters.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterDynamicFilters.php new file mode 100644 index 00000000000..22ff5ca9681 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterDynamicFilters.php @@ -0,0 +1,207 @@ +doctrineHelper = $doctrineHelper; + $this->configProvider = $configProvider; + } + + /** + * {@inheritdoc} + */ + public function process(ContextInterface $context) + { + /** @var Context $context */ + + $allFilterValues = $context->getFilterValues(); + $filterValues = $allFilterValues->getGroup('filter'); + if (!empty($filterValues)) { + $filters = $context->getFilters(); + $knownFilterKeys = []; + foreach ($filters as $filterKey => $filter) { + if ($allFilterValues->has($filterKey)) { + $knownFilterKeys[$filterKey] = true; + } + } + foreach ($filterValues as $filterKey => $filterValue) { + if (isset($knownFilterKeys[$filterKey])) { + continue; + } + + $filter = $this->getFilter($filterValue->getPath(), $context); + if ($filter) { + $filters->add($filterKey, $filter); + } else { + $context->addError( + Error::createValidationError( + Constraint::FILTER, + sprintf('Filter "%s" is not supported.', $filterKey) + )->setSource(ErrorSource::createByParameter($filterKey)) + ); + } + } + } + } + + /** + * @param string $propertyPath + * @param Context $context + * + * @return StandaloneFilter|null + */ + protected function getFilter($propertyPath, Context $context) + { + /** @var ClassMetadata $metadata */ + $metadata = $this->doctrineHelper->getEntityMetadataForClass($context->getClassName(), false); + if (!$metadata) { + return null; + } + + $filterInfo = $this->getFilterInfo($propertyPath, $metadata, $context); + if (null === $filterInfo) { + return null; + } + + list($filterConfig, $propertyPath, $isCollection) = $filterInfo; + $filter = $this->createFilter($filterConfig, $propertyPath); + if (null !== $filter) { + // @todo BAP-11881. Update this code when NEQ operator for to-many collection + // will be implemented in Oro\Bundle\ApiBundle\Filter\ComparisonFilter + if ($isCollection) { + $filter->setSupportedOperators([StandaloneFilter::EQ]); + } + } + + return $filter; + } + + /** + * @param string $propertyPath + * @param ClassMetadata $metadata + * @param Context $context + * + * @return array|null [filter config, property path, is collection] + */ + protected function getFilterInfo($propertyPath, ClassMetadata $metadata, Context $context) + { + $filtersConfig = null; + $associationPropertyPath = null; + $isCollection = false; + + $path = explode('.', $propertyPath); + if (count($path) > 1) { + $fieldName = array_pop($path); + $associationInfo = $this->getAssociationInfo($path, $context, $metadata); + if (null !== $associationInfo) { + list($filtersConfig, $associationPropertyPath, $isCollection) = $associationInfo; + } + } else { + $fieldName = $propertyPath; + $filtersConfig = $context->getConfigOfFilters(); + } + + $result = null; + if ($filtersConfig) { + $filterConfig = $filtersConfig->getField($fieldName); + if ($filterConfig) { + $propertyPath = $filterConfig->getPropertyPath($fieldName); + if ($associationPropertyPath) { + $propertyPath = $associationPropertyPath . '.' . $propertyPath; + } + $result = [$filterConfig, $propertyPath, $isCollection]; + } + } + + return $result; + } + + /** + * @param string[] $path + * @param Context $context + * @param ClassMetadata $metadata + * + * @return array|null [filters config, association property path, is collection] + */ + protected function getAssociationInfo(array $path, Context $context, ClassMetadata $metadata) + { + $targetConfigExtras = [ + new EntityDefinitionConfigExtra($context->getAction()), + new FiltersConfigExtra() + ]; + + $config = $context->getConfig(); + $filters = null; + $associationPath = []; + $isCollection = false; + + foreach ($path as $fieldName) { + $field = $config->getField($fieldName); + if (null === $field) { + return null; + } + + $associationPropertyPath = $field->getPropertyPath($fieldName); + if (!$metadata->hasAssociation($associationPropertyPath)) { + return null; + } + + $targetClass = $metadata->getAssociationTargetClass($associationPropertyPath); + $targetConfig = $this->configProvider->getConfig( + $targetClass, + $context->getVersion(), + $context->getRequestType(), + $targetConfigExtras + ); + if (!$targetConfig->hasDefinition()) { + return null; + } + + if ($metadata->isCollectionValuedAssociation($associationPropertyPath)) { + $isCollection = true; + } + + $metadata = $this->doctrineHelper->getEntityMetadataForClass($targetClass); + $config = $targetConfig->getDefinition(); + $filters = $targetConfig->getFilters(); + $associationPath[] = $associationPropertyPath; + } + + return [$filters, implode('.', $associationPath), $isCollection]; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterFilters.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterFilters.php index 62b5b6e35c9..4f7b60ecdec 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterFilters.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/RegisterFilters.php @@ -2,19 +2,13 @@ namespace Oro\Bundle\ApiBundle\Processor\Shared; -use Oro\Component\ChainProcessor\ContextInterface; use Oro\Component\ChainProcessor\ProcessorInterface; use Oro\Bundle\ApiBundle\Config\FilterFieldConfig; -use Oro\Bundle\ApiBundle\Exception\RuntimeException; -use Oro\Bundle\ApiBundle\Filter\ComparisonFilter; +use Oro\Bundle\ApiBundle\Filter\FieldAwareFilterInterface; use Oro\Bundle\ApiBundle\Filter\FilterFactoryInterface; use Oro\Bundle\ApiBundle\Filter\StandaloneFilter; -use Oro\Bundle\ApiBundle\Processor\Context; -/** - * Registers filters according to the "filters" configuration section. - */ -class RegisterFilters implements ProcessorInterface +abstract class RegisterFilters implements ProcessorInterface { /** @var FilterFactoryInterface */ protected $filterFactory; @@ -28,55 +22,34 @@ public function __construct(FilterFactoryInterface $filterFactory) } /** - * {@inheritdoc} - */ - public function process(ContextInterface $context) - { - /** @var Context $context */ - - $configOfFilters = $context->getConfigOfFilters(); - if (null === $configOfFilters || $configOfFilters->isEmpty()) { - // a filters' configuration does not contains any data - return; - } - - if (!$configOfFilters->isExcludeAll()) { - // it seems that filters' configuration was not normalized - throw new RuntimeException( - sprintf( - 'Expected "all" exclusion policy for filters. Got: %s.', - $configOfFilters->getExclusionPolicy() - ) - ); - } - - $filters = $context->getFilters(); - $fields = $configOfFilters->getFields(); - foreach ($fields as $fieldName => $field) { - if ($filters->has($fieldName)) { - continue; - } - $filter = $this->createFilter($fieldName, $field); - if (null !== $filter) { - $filters->add($fieldName, $filter); - } - } - } - - /** - * @param string $fieldName - * @param FilterFieldConfig $field + * @param FilterFieldConfig $filterConfig + * @param string $propertyPath * * @return StandaloneFilter|null */ - protected function createFilter($fieldName, FilterFieldConfig $field) + protected function createFilter(FilterFieldConfig $filterConfig, $propertyPath) { - $filter = $this->filterFactory->createFilter($field->getDataType()); + $filterOptions = $filterConfig->getOptions(); + if (null === $filterOptions) { + $filterOptions = []; + } + $filterType = $filterConfig->getType(); + $dataType = $filterConfig->getDataType(); + if (!$filterType) { + $filterType = $dataType; + } elseif ($filterType !== $dataType) { + $filterOptions[FilterFactoryInterface::DATA_TYPE_OPTION] = $dataType; + } + $filter = $this->filterFactory->createFilter($filterType, $filterOptions); if (null !== $filter) { - $filter->setArrayAllowed($field->isArrayAllowed()); - $filter->setDescription($field->getDescription()); - if ($filter instanceof ComparisonFilter) { - $filter->setField($field->getPropertyPath() ?: $fieldName); + $filter->setArrayAllowed($filterConfig->isArrayAllowed()); + $filter->setDescription($filterConfig->getDescription()); + $operators = $filterConfig->getOperators(); + if (!empty($operators)) { + $filter->setSupportedOperators($operators); + } + if ($filter instanceof FieldAwareFilterInterface) { + $filter->setField($propertyPath); } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/RestoreDefaultFormExtension.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/RestoreDefaultFormExtension.php index 7fc52f55c31..05bc475e58a 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/RestoreDefaultFormExtension.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/RestoreDefaultFormExtension.php @@ -43,5 +43,6 @@ public function process(ContextInterface $context) $this->formExtensionSwitcher->switchToDefaultFormExtension(); $this->metadataTypeGuesser->setMetadataAccessor(); + $this->metadataTypeGuesser->setConfigAccessor(); } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/ValidateSorting.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/ValidateSorting.php index 96dd34ccd54..1d9b8b3d0b6 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/ValidateSorting.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/ValidateSorting.php @@ -159,7 +159,7 @@ protected function validateSorter($fieldName, SortersConfig $sorters = null) return null; } - return $sorter->getPropertyPath() ?: $fieldName; + return $sorter->getPropertyPath($fieldName); } /** @@ -208,7 +208,7 @@ protected function getAssociationSorters(array $path, Context $context, ClassMet return [null, null]; } - $associationName = $config->getField($fieldName)->getPropertyPath() ?: $fieldName; + $associationName = $config->getField($fieldName)->getPropertyPath($fieldName); if (!$metadata->hasAssociation($associationName)) { return [null, null]; } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Subresource/ContextParentConfigAccessor.php b/src/Oro/Bundle/ApiBundle/Processor/Subresource/ContextParentConfigAccessor.php new file mode 100644 index 00000000000..2f7047eca48 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Processor/Subresource/ContextParentConfigAccessor.php @@ -0,0 +1,29 @@ +context = $context; + } + + /** + * {@inheritdoc} + */ + public function getConfig($className) + { + return $this->context->getParentClassName() === $className + ? $this->context->getParentConfig() + : null; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/InitializeApiFormExtension.php b/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/InitializeApiFormExtension.php index 3203ad95a2b..3c9850b253c 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/InitializeApiFormExtension.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/InitializeApiFormExtension.php @@ -6,6 +6,7 @@ use Oro\Component\ChainProcessor\ProcessorInterface; use Oro\Bundle\ApiBundle\Form\FormExtensionSwitcherInterface; use Oro\Bundle\ApiBundle\Form\Guesser\MetadataTypeGuesser; +use Oro\Bundle\ApiBundle\Processor\Subresource\ContextParentConfigAccessor; use Oro\Bundle\ApiBundle\Processor\Subresource\ContextParentMetadataAccessor; use Oro\Bundle\ApiBundle\Processor\Subresource\SubresourceContext; @@ -41,5 +42,6 @@ public function process(ContextInterface $context) $this->formExtensionSwitcher->switchToApiFormExtension(); $this->metadataTypeGuesser->setMetadataAccessor(new ContextParentMetadataAccessor($context)); + $this->metadataTypeGuesser->setConfigAccessor(new ContextParentConfigAccessor($context)); } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/RecognizeAssociationType.php b/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/RecognizeAssociationType.php index 5d8ef2e1564..d96b323938d 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/RecognizeAssociationType.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/RecognizeAssociationType.php @@ -53,23 +53,7 @@ public function process(ContextInterface $context) return; } - $version = $context->getVersion(); - $requestType = $context->getRequestType(); - $parentEntityClass = $context->getParentClassName(); - - $entitySubresources = $this->subresourcesProvider->getSubresources( - $parentEntityClass, - $version, - $requestType - ); - $subresource = $entitySubresources - ? $entitySubresources->getSubresource($associationName) - : null; - - if ($subresource) { - $context->setClassName($subresource->getTargetClassName()); - $context->setIsCollection($subresource->isCollection()); - } else { + if (!$this->setAssociationType($context, $associationName)) { $context->addError( Error::createValidationError( Constraint::RELATIONSHIP, @@ -78,4 +62,35 @@ public function process(ContextInterface $context) ); } } + + /** + * @param SubresourceContext $context + * @param string $associationName + * + * @return bool + */ + protected function setAssociationType(SubresourceContext $context, $associationName) + { + $entitySubresources = $this->subresourcesProvider->getSubresources( + $context->getParentClassName(), + $context->getVersion(), + $context->getRequestType() + ); + if (null === $entitySubresources) { + return false; + } + $subresource = $entitySubresources->getSubresource($associationName); + if (null === $subresource) { + return false; + } + $targetClassName = $subresource->getTargetClassName(); + if (!$targetClassName) { + return false; + } + + $context->setClassName($targetClassName); + $context->setIsCollection($subresource->isCollection()); + + return true; + } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/SaveParentEntity.php b/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/SaveParentEntity.php index 5d3bf506e2d..67c54376624 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/SaveParentEntity.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Subresource/Shared/SaveParentEntity.php @@ -42,6 +42,6 @@ public function process(ContextInterface $context) return; } - $em->flush($parentEntity); + $em->flush(); } } diff --git a/src/Oro/Bundle/ApiBundle/Processor/Update/SaveEntity.php b/src/Oro/Bundle/ApiBundle/Processor/Update/SaveEntity.php index 4dbaa66febb..2a7551a11a4 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Update/SaveEntity.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Update/SaveEntity.php @@ -42,6 +42,6 @@ public function process(ContextInterface $context) return; } - $em->flush($entity); + $em->flush(); } } diff --git a/src/Oro/Bundle/ApiBundle/Provider/MetadataProvider.php b/src/Oro/Bundle/ApiBundle/Provider/MetadataProvider.php index 386a50e4a5a..0b1daa3edb3 100644 --- a/src/Oro/Bundle/ApiBundle/Provider/MetadataProvider.php +++ b/src/Oro/Bundle/ApiBundle/Provider/MetadataProvider.php @@ -28,11 +28,13 @@ public function __construct(ActionProcessorInterface $processor) /** * Gets metadata for the given version of an entity. * - * @param string $className The FQCN of an entity - * @param string $version The version of a config - * @param RequestType $requestType The request type, for example "rest", "soap", etc. - * @param EntityDefinitionConfig $config The configuration of an entity - * @param MetadataExtraInterface[] $extras Requests for additional metadata information + * @param string $className The FQCN of an entity + * @param string $version The version of a config + * @param RequestType $requestType The request type, for example "rest", "soap", etc. + * @param EntityDefinitionConfig $config The configuration of an entity + * @param MetadataExtraInterface[] $extras Requests for additional metadata information + * @param bool $withExcludedProperties Whether excluded fields and associations + * should not be removed * * @return EntityMetadata|null */ @@ -41,7 +43,8 @@ public function getMetadata( $version, RequestType $requestType, EntityDefinitionConfig $config, - array $extras = [] + array $extras = [], + $withExcludedProperties = false ) { if (empty($className)) { throw new \InvalidArgumentException('$className must not be empty.'); @@ -49,15 +52,36 @@ public function getMetadata( $configKey = $config->getKey(); if (!$configKey) { - return $this->loadMetadata($className, $version, $requestType, $config, $extras); + return $this->loadMetadata( + $className, + $version, + $requestType, + $config, + $extras, + $withExcludedProperties + ); } - $cacheKey = $this->buildCacheKey($className, $version, $requestType, $extras, $configKey); + $cacheKey = $this->buildCacheKey( + $className, + $version, + $requestType, + $extras, + $withExcludedProperties, + $configKey + ); if (array_key_exists($cacheKey, $this->cache)) { return clone $this->cache[$cacheKey]; } - $metadata = $this->loadMetadata($className, $version, $requestType, $config, $extras); + $metadata = $this->loadMetadata( + $className, + $version, + $requestType, + $config, + $extras, + $withExcludedProperties + ); $this->cache[$cacheKey] = $metadata; if (null === $metadata) { @@ -73,6 +97,7 @@ public function getMetadata( * @param RequestType $requestType * @param EntityDefinitionConfig $config * @param MetadataExtraInterface[] $extras + * @param bool $withExcludedProperties * * @return EntityMetadata|null */ @@ -81,7 +106,8 @@ protected function loadMetadata( $version, RequestType $requestType, EntityDefinitionConfig $config, - array $extras + array $extras, + $withExcludedProperties ) { /** @var MetadataContext $context */ $context = $this->processor->createContext(); @@ -92,6 +118,7 @@ protected function loadMetadata( if (!empty($extras)) { $context->setExtras($extras); } + $context->setWithExcludedProperties($withExcludedProperties); $this->processor->process($context); @@ -108,13 +135,23 @@ protected function loadMetadata( * @param string $version * @param RequestType $requestType * @param MetadataExtraInterface[] $extras + * @param bool $withExcludedProperties * @param string $configKey * * @return string */ - protected function buildCacheKey($className, $version, RequestType $requestType, array $extras, $configKey) - { - $cacheKey = (string)$requestType . '|' . $version . '|' . $className; + protected function buildCacheKey( + $className, + $version, + RequestType $requestType, + array $extras, + $withExcludedProperties, + $configKey + ) { + $cacheKey = (string)$requestType + . '|' . $version + . '|' . $className + . '|' . ($withExcludedProperties ? '1' : '0'); foreach ($extras as $extra) { $part = $extra->getCacheKeyPart(); if (!empty($part)) { diff --git a/src/Oro/Bundle/ApiBundle/Provider/ResourcesCache.php b/src/Oro/Bundle/ApiBundle/Provider/ResourcesCache.php index 9a5f5aeed99..5ba01f9fcca 100644 --- a/src/Oro/Bundle/ApiBundle/Provider/ResourcesCache.php +++ b/src/Oro/Bundle/ApiBundle/Provider/ResourcesCache.php @@ -4,7 +4,6 @@ use Doctrine\Common\Cache\CacheProvider; -use Oro\Bundle\ApiBundle\Request\ApiActions; use Oro\Bundle\ApiBundle\Request\ApiResource; use Oro\Bundle\ApiBundle\Request\ApiResourceSubresources; use Oro\Bundle\ApiBundle\Request\RequestType; @@ -32,7 +31,7 @@ public function __construct(CacheProvider $cache) * @param string $version The Data API version * @param RequestType $requestType The request type, for example "rest", "soap", etc. * - * @return string[]|null The list of entity classes accessible through Data API or NULL if it is not cached yet + * @return array|null [entity class => accessible flag] or NULL if the list is not cached yet */ public function getAccessibleResources($version, RequestType $requestType) { @@ -94,25 +93,26 @@ public function getSubresources($entityClass, $version, RequestType $requestType /** * Puts Data API resources into the cache. * - * @param string $version The Data API version - * @param RequestType $requestType The request type, for example "rest", "soap", etc. - * @param ApiResource[] $resources The list of Data API resources + * @param string $version The Data API version + * @param RequestType $requestType The request type, for example "rest", "soap", etc. + * @param ApiResource[] $resources The list of Data API resources + * @param string[] $accessibleResources The list of resources accessible through Data API */ - public function saveResources($version, RequestType $requestType, array $resources) + public function saveResources($version, RequestType $requestType, array $resources, array $accessibleResources) { $allResources = []; - $accessibleResources = []; + $accessibleResourcesData = array_fill_keys($accessibleResources, true); foreach ($resources as $resource) { $entityClass = $resource->getEntityClass(); $allResources[$entityClass] = $this->serializeApiResource($resource); - if (!in_array(ApiActions::GET, $resource->getExcludedActions(), true)) { - $accessibleResources[] = $entityClass; + if (!isset($accessibleResourcesData[$entityClass])) { + $accessibleResourcesData[$entityClass] = false; } } $keyIndex = $this->getCacheKeyIndex($version, $requestType); $this->cache->save(self::RESOURCES_KEY_PREFIX . $keyIndex, $allResources); - $this->cache->save(self::ACCESSIBLE_RESOURCES_KEY_PREFIX . $keyIndex, $accessibleResources); + $this->cache->save(self::ACCESSIBLE_RESOURCES_KEY_PREFIX . $keyIndex, $accessibleResourcesData); } /** diff --git a/src/Oro/Bundle/ApiBundle/Provider/ResourcesProvider.php b/src/Oro/Bundle/ApiBundle/Provider/ResourcesProvider.php index 4c0497bb574..0e05912a4dd 100644 --- a/src/Oro/Bundle/ApiBundle/Provider/ResourcesProvider.php +++ b/src/Oro/Bundle/ApiBundle/Provider/ResourcesProvider.php @@ -29,7 +29,7 @@ public function __construct(ActionProcessorInterface $processor, ResourcesCache } /** - * Gets all resources available through a given Data API version. + * Gets a configuration of all resources for a given Data API version. * * @param string $version The Data API version * @param RequestType $requestType The request type, for example "rest", "soap", etc. @@ -51,31 +51,87 @@ public function getResources($version, RequestType $requestType) $this->processor->process($context); $resources = array_values($context->getResult()->toArray()); - $this->resourcesCache->saveResources($version, $requestType, $resources); + $this->resourcesCache->saveResources( + $version, + $requestType, + $resources, + $context->getAccessibleResources() + ); return $resources; } /** - * Checks whether a given entity type is accessible through Data API. + * Gets a list of resources accessible through Data API. * - * @param string $entityClass - * @param string $version - * @param RequestType $requestType + * @param string $version The Data API version + * @param RequestType $requestType The request type, for example "rest", "soap", etc. + * + * @return string[] The list of class names + */ + public function getAccessibleResources($version, RequestType $requestType) + { + $result = []; + $accessibleResources = $this->loadAccessibleResources($version, $requestType); + foreach ($accessibleResources as $entityClass => $isAccessible) { + if ($isAccessible) { + $result[] = $entityClass; + } + } + + return $result; + } + + /** + * Checks whether a given entity is accessible through Data API. + * + * @param string $entityClass The FQCN of an entity + * @param string $version The Data API version + * @param RequestType $requestType The request type, for example "rest", "soap", etc. * * @return bool */ public function isResourceAccessible($entityClass, $version, RequestType $requestType) + { + $accessibleResources = $this->loadAccessibleResources($version, $requestType); + + return + array_key_exists($entityClass, $accessibleResources) + && $accessibleResources[$entityClass]; + } + + /** + * Checks whether a given entity is configured to be used in Data API. + * + * @param string $entityClass The FQCN of an entity + * @param string $version The Data API version + * @param RequestType $requestType The request type, for example "rest", "soap", etc. + * + * @return bool + */ + public function isResourceKnown($entityClass, $version, RequestType $requestType) + { + $accessibleResources = $this->loadAccessibleResources($version, $requestType); + + return array_key_exists($entityClass, $accessibleResources); + } + + /** + * @param string $version + * @param RequestType $requestType + * + * @return array [entity class => accessible flag] + */ + protected function loadAccessibleResources($version, RequestType $requestType) { if (null === $this->accessibleResources) { - $accessibleResources = $this->resourcesCache->getAccessibleResources($version, $requestType); - if (null === $accessibleResources) { + $this->accessibleResources = $this->resourcesCache->getAccessibleResources($version, $requestType); + if (null === $this->accessibleResources) { $this->getResources($version, $requestType); - $accessibleResources = $this->resourcesCache->getAccessibleResources($version, $requestType); + $this->accessibleResources = $this->resourcesCache->getAccessibleResources($version, $requestType); } - $this->accessibleResources = array_fill_keys($accessibleResources, true); } - return isset($this->accessibleResources[$entityClass]); + return $this->accessibleResources; } } diff --git a/src/Oro/Bundle/ApiBundle/Provider/SubresourcesProvider.php b/src/Oro/Bundle/ApiBundle/Provider/SubresourcesProvider.php index beb454bb84b..67f4562b8c7 100644 --- a/src/Oro/Bundle/ApiBundle/Provider/SubresourcesProvider.php +++ b/src/Oro/Bundle/ApiBundle/Provider/SubresourcesProvider.php @@ -54,6 +54,7 @@ public function getSubresources($entityClass, $version, RequestType $requestType $context->setVersion($version); $context->getRequestType()->set($requestType); $context->setResources($this->resourcesProvider->getResources($version, $requestType)); + $context->setAccessibleResources($this->resourcesProvider->getAccessibleResources($version, $requestType)); $this->processor->process($context); diff --git a/src/Oro/Bundle/ApiBundle/Request/AbstractDocumentBuilder.php b/src/Oro/Bundle/ApiBundle/Request/AbstractDocumentBuilder.php index 502cb5973e7..1a686b324f3 100644 --- a/src/Oro/Bundle/ApiBundle/Request/AbstractDocumentBuilder.php +++ b/src/Oro/Bundle/ApiBundle/Request/AbstractDocumentBuilder.php @@ -182,18 +182,6 @@ protected function hasIdentifierFieldsOnly(EntityMetadata $metadata) return $metadata->hasIdentifierFieldsOnly(); } - /** - * Checks whether a given association should be represented as an array attribute. - * - * @param AssociationMetadata $association - * - * @return bool - */ - protected function isArrayAttribute(AssociationMetadata $association) - { - return 'array' === $association->getDataType(); - } - /** * @return AssociationToArrayAttributeConverter */ @@ -241,16 +229,13 @@ public function getRelationshipValue(array $data, $associationName, AssociationM if (array_key_exists($associationName, $data)) { $val = $data[$associationName]; if (!$this->isEmptyRelationship($val, $isCollection)) { - $isArrayAttribute = $this->isArrayAttribute($association); - if ($isCollection) { - $result = $isArrayAttribute - ? $this->getArrayAttributeConverter() - ->convertCollectionToArray($val, $association->getTargetMetadata()) - : $this->processRelatedCollection($val, $association); + if (DataType::isAssociationAsField($association->getDataType())) { + $result = $isCollection + ? $this->getArrayAttributeConverter()->convertCollectionToArray($val, $association) + : $this->getArrayAttributeConverter()->convertObjectToArray($val, $association); } else { - $result = $isArrayAttribute - ? $this->getArrayAttributeConverter() - ->convertObjectToArray($val, $association->getTargetMetadata()) + $result = $isCollection + ? $this->processRelatedCollection($val, $association) : $this->processRelatedObject($val, $association); } } diff --git a/src/Oro/Bundle/ApiBundle/Request/DataType.php b/src/Oro/Bundle/ApiBundle/Request/DataType.php index a3f0beb51cc..4ee565f49fe 100644 --- a/src/Oro/Bundle/ApiBundle/Request/DataType.php +++ b/src/Oro/Bundle/ApiBundle/Request/DataType.php @@ -20,4 +20,19 @@ final class DataType const ENTITY_TYPE = 'entityType'; const ENTITY_CLASS = 'entityClass'; const ORDER_BY = 'orderBy'; + + /** + * Checks whether the association should be represented as a field. + * For JSON.API it means that it should be in "attributes" section instead of "relationships" section. + * Usually, to increase readability, "array" data type is used for "to-many" associations + * and "scalar" data type is used for "to-one" associations. + * + * @param string $dataType + * + * @return bool + */ + public static function isAssociationAsField($dataType) + { + return in_array($dataType, ['array', 'scalar'], true); + } } diff --git a/src/Oro/Bundle/ApiBundle/Request/DocumentBuilder/AssociationToArrayAttributeConverter.php b/src/Oro/Bundle/ApiBundle/Request/DocumentBuilder/AssociationToArrayAttributeConverter.php index 46efe7dde48..4c9c2758ccc 100644 --- a/src/Oro/Bundle/ApiBundle/Request/DocumentBuilder/AssociationToArrayAttributeConverter.php +++ b/src/Oro/Bundle/ApiBundle/Request/DocumentBuilder/AssociationToArrayAttributeConverter.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\ApiBundle\Request\DocumentBuilder; +use Oro\Bundle\ApiBundle\Metadata\AssociationMetadata; use Oro\Bundle\ApiBundle\Metadata\EntityMetadata; class AssociationToArrayAttributeConverter @@ -18,30 +19,54 @@ public function __construct(ObjectAccessorInterface $objectAccessor) } /** - * @param mixed $object - * @param EntityMetadata|null $metadata + * @param mixed $object + * @param AssociationMetadata|null $association * * @return array */ - public function convertObjectToArray($object, EntityMetadata $metadata = null) + public function convertObjectToArray($object, AssociationMetadata $association = null) { if (null === $object || is_scalar($object)) { return $object; } - if (null === $metadata) { + if (null === $association) { $result = $this->objectAccessor->toArray($object); } else { - $data = $this->objectAccessor->toArray($object); - if ($metadata->hasIdentifierFieldsOnly()) { - $result = count($data) === 1 - ? reset($data) - : $data; + $metadata = $association->getTargetMetadata(); + if (null === $metadata) { + $result = $this->objectAccessor->toArray($object); } else { - $result = []; - $this->addMeta($result, $data, $metadata); - $this->addAttributes($result, $data, $metadata); - $this->addRelationships($result, $data, $metadata); + $data = $this->objectAccessor->toArray($object); + if ($metadata->hasIdentifierFieldsOnly()) { + $idFieldNames = $metadata->getIdentifierFieldNames(); + if (1 === count($idFieldNames)) { + $fieldName = reset($idFieldNames); + $result = array_key_exists($fieldName, $data) + ? $data[$fieldName] + : null; + } else { + $result = []; + foreach ($idFieldNames as $fieldName) { + if (array_key_exists($fieldName, $data)) { + $result[$fieldName] = $data[$fieldName]; + } + } + } + } else { + $result = []; + $this->addMeta($result, $data, $metadata); + $this->addAttributes($result, $data, $metadata); + $this->addRelationships($result, $data, $metadata); + if ($association->isCollapsed()) { + $count = count($result); + if (1 === $count) { + $result = reset($result); + } elseif (1 === $count) { + $result = null; + } + } + } } } @@ -49,16 +74,16 @@ public function convertObjectToArray($object, EntityMetadata $metadata = null) } /** - * @param array|\Traversable $collection - * @param EntityMetadata|null $metadata + * @param array|\Traversable $collection + * @param AssociationMetadata|null $association * * @return array */ - public function convertCollectionToArray($collection, EntityMetadata $metadata = null) + public function convertCollectionToArray($collection, AssociationMetadata $association = null) { $result = []; foreach ($collection as $object) { - $result[] = $this->convertObjectToArray($object, $metadata); + $result[] = $this->convertObjectToArray($object, $association); } return $result; @@ -109,8 +134,8 @@ protected function addRelationships(array &$result, array $data, EntityMetadata $val = $data[$name]; if (!$this->isEmptyRelationship($val, $isCollection)) { $value = $isCollection - ? $this->convertCollectionToArray($val, $association->getTargetMetadata()) - : $this->convertObjectToArray($val, $association->getTargetMetadata()); + ? $this->convertCollectionToArray($val, $association) + : $this->convertObjectToArray($val, $association); } } if (null === $value && $isCollection) { diff --git a/src/Oro/Bundle/ApiBundle/Request/JsonApi/ErrorCompleter.php b/src/Oro/Bundle/ApiBundle/Request/JsonApi/ErrorCompleter.php index 8fd1bdecfa0..e8e2bdb0865 100644 --- a/src/Oro/Bundle/ApiBundle/Request/JsonApi/ErrorCompleter.php +++ b/src/Oro/Bundle/ApiBundle/Request/JsonApi/ErrorCompleter.php @@ -5,6 +5,7 @@ use Oro\Bundle\ApiBundle\Metadata\EntityMetadata; use Oro\Bundle\ApiBundle\Model\Error; use Oro\Bundle\ApiBundle\Request\AbstractErrorCompleter; +use Oro\Bundle\ApiBundle\Request\DataType; use Oro\Bundle\ApiBundle\Request\JsonApi\JsonApiDocumentBuilder as JsonApiDoc; class ErrorCompleter extends AbstractErrorCompleter @@ -42,9 +43,17 @@ public function completeSource(Error $error, EntityMetadata $metadata = null) } else { $parts = explode('.', $propertyPath); if (array_key_exists($parts[0], $metadata->getAssociations())) { - $pointer = [JsonApiDoc::RELATIONSHIPS, $parts[0], JsonApiDoc::DATA]; + $association = $metadata->getAssociation($parts[0]); + $pointer = DataType::isAssociationAsField($association->getDataType()) + ? [JsonApiDoc::ATTRIBUTES, $parts[0]] + : [JsonApiDoc::RELATIONSHIPS, $parts[0], JsonApiDoc::DATA]; if (count($parts) > 1) { $pointer[] = $parts[1]; + if (DataType::isAssociationAsField($association->getDataType()) + && !$association->isCollapsed() + ) { + $pointer = array_merge($pointer, array_slice($parts, 2)); + } } } else { $error->setDetail($this->appendSourceToMessage($error->getDetail(), $propertyPath)); diff --git a/src/Oro/Bundle/ApiBundle/Request/JsonApi/JsonApiDocumentBuilder.php b/src/Oro/Bundle/ApiBundle/Request/JsonApi/JsonApiDocumentBuilder.php index 2c613c27760..af68603f78d 100644 --- a/src/Oro/Bundle/ApiBundle/Request/JsonApi/JsonApiDocumentBuilder.php +++ b/src/Oro/Bundle/ApiBundle/Request/JsonApi/JsonApiDocumentBuilder.php @@ -6,6 +6,7 @@ use Oro\Bundle\ApiBundle\Metadata\EntityMetadata; use Oro\Bundle\ApiBundle\Model\Error; use Oro\Bundle\ApiBundle\Request\AbstractDocumentBuilder; +use Oro\Bundle\ApiBundle\Request\DataType; use Oro\Bundle\ApiBundle\Request\DocumentBuilder\EntityIdAccessor; use Oro\Bundle\ApiBundle\Request\EntityIdTransformerInterface; use Oro\Bundle\ApiBundle\Request\RequestType; @@ -172,7 +173,7 @@ protected function addRelationships(array &$result, array $data, EntityMetadata $associations = $metadata->getAssociations(); foreach ($associations as $name => $association) { $value = $this->getRelationshipValue($data, $name, $association); - if ($this->isArrayAttribute($association)) { + if (DataType::isAssociationAsField($association->getDataType())) { $result[self::ATTRIBUTES][$name] = $value; } else { $result[self::RELATIONSHIPS][$name][self::DATA] = $value; diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/form.yml b/src/Oro/Bundle/ApiBundle/Resources/config/form.yml index 62cb3b0acb0..95149c9171f 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/form.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/form.yml @@ -14,10 +14,18 @@ services: - [] # All services with tag "oro.api.form.type_extension" are inserted here by ConfigurationCompilerPass - [] # All services with tag "oro.api.form.type_guesser" are inserted here by ConfigurationCompilerPass + oro_api.form.extension.customize_form_data: + class: Oro\Bundle\ApiBundle\Form\Extension\CustomizeFormDataExtension + arguments: + - '@oro_api.customize_form_data.processor' + tags: + - { name: oro.api.form.type_extension, alias: form, extended_type: 'Symfony\Component\Form\Extension\Core\Type\FormType' } + oro_api.form.guesser.metadata: class: Oro\Bundle\ApiBundle\Form\Guesser\MetadataTypeGuesser arguments: - [] # Data type mappings are inserted here by ConfigurationCompilerPass + - '@oro_api.doctrine_helper' tags: - { name: oro.api.form.type_guesser, priority: 10 } @@ -33,6 +41,31 @@ services: tags: - { name: oro.api.form.type, alias: oro_api_entity } + oro_api.form.type.compound_entity: + class: Oro\Bundle\ApiBundle\Form\Type\CompoundEntityType + tags: + - { name: oro.api.form.type, alias: oro_api_compound_entity } + + oro_api.form.type.collection: + class: Oro\Bundle\ApiBundle\Form\Type\CollectionType + tags: + - { name: oro.api.form.type, alias: oro_api_collection } + + oro_api.form.type.collection.entity: + class: Oro\Bundle\ApiBundle\Form\Type\EntityCollectionType + tags: + - { name: oro.api.form.type, alias: oro_api_entity_collection } + + oro_api.form.type.scalar_collection: + class: Oro\Bundle\ApiBundle\Form\Type\ScalarCollectionType + tags: + - { name: oro.api.form.type, alias: oro_api_scalar_collection } + + oro_api.form.type.scalar_collection.entity: + class: Oro\Bundle\ApiBundle\Form\Type\EntityScalarCollectionType + tags: + - { name: oro.api.form.type, alias: oro_api_entity_scalar_collection } + oro_api.validator.access_granted: class: Oro\Bundle\ApiBundle\Validator\Constraints\AccessGrantedValidator arguments: diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/oro/api.yml b/src/Oro/Bundle/ApiBundle/Resources/config/oro/api.yml index 3300fc4fe1d..ca0385fe9bc 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/oro/api.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/oro/api.yml @@ -15,14 +15,7 @@ oro_api: id: data_type: string # this entity does not have own Data API resource - actions: - get: false - get_list: false - update: false - create: false - delete: false - delete_list: false - + actions: false Oro\Bundle\ApiBundle\Model\EntityDescriptor: identifier_field_names: [id] disable_fieldset: true @@ -44,10 +37,4 @@ oro_api: meta_property: true # this entity does not have own Data API resource # and should be used only as a sub-resource of other entities - actions: - get: false - get_list: false - update: false - create: false - delete: false - delete_list: false + actions: false diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/oro/app.yml b/src/Oro/Bundle/ApiBundle/Resources/config/oro/app.yml index 25e684a81eb..4dd8525f05b 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/oro/app.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/oro/app.yml @@ -25,6 +25,8 @@ security: context: main oro_api: + api_doc_views: ['default', 'rest_json_api', 'rest_plain'] + filters: string: ~ boolean: ~ @@ -40,6 +42,8 @@ oro_api: supported_operators: ['=', '!=', '<', '<=', '>', '>='] datetime: supported_operators: ['=', '!=', '<', '<=', '>', '>='] + primaryField: + class: Oro\Bundle\ApiBundle\Filter\PrimaryFieldFilter form_types: - form.type.form @@ -69,6 +73,7 @@ oro_api: - form.type_extension.repeated.validator - form.type_extension.form.data_collector - oro_security.form.extension.aclprotected_fields_type + - oro_form.extension.constraint_as_option form_type_guessers: - form.type_guesser.validator diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_resources.yml b/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_resources.yml index 90932bd301d..7cbfb9426f6 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_resources.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_resources.yml @@ -53,3 +53,8 @@ services: - ['delete', 'delete_list', 'create', 'update'] tags: - { name: oro.api.processor, action: collect_resources, priority: -120 } + + oro_api.collect_resources.load_accessible_resources: + class: Oro\Bundle\ApiBundle\Processor\CollectResources\LoadAccessibleResources + tags: + - { name: oro.api.processor, action: collect_resources, priority: -200 } diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_subresources.yml b/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_subresources.yml index 62d217f2735..b02db3429a5 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_subresources.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/processors.collect_subresources.yml @@ -23,5 +23,7 @@ services: arguments: - '@oro_api.config_loader_factory' - '@oro_api.config_bag' + - '@oro_api.config_provider' + - '@oro_api.metadata_provider' tags: - { name: oro.api.processor, action: collect_subresources, priority: -10 } diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/processors.delete_list.yml b/src/Oro/Bundle/ApiBundle/Resources/config/processors.delete_list.yml index d2930fffd9f..df438f8059d 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/processors.delete_list.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/processors.delete_list.yml @@ -37,10 +37,11 @@ services: tags: - { name: oro.api.processor, action: delete_list, group: initialize, priority: 10 } - oro_api.delete_list.register_filters: - class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterFilters + oro_api.delete_list.register_configured_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterConfiguredFilters arguments: - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: delete_list, group: initialize, priority: -50 } @@ -51,6 +52,15 @@ services: tags: - { name: oro.api.processor, action: delete_list, group: initialize, requestType: json_api, priority: -55 } + oro_api.delete_list.register_dynamic_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterDynamicFilters + arguments: + - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' + - '@oro_api.config_provider' + tags: + - { name: oro.api.processor, action: delete_list, group: initialize, priority: -60 } + # # normalize_input # @@ -99,9 +109,6 @@ services: oro_api.delete_list.build_criteria: class: Oro\Bundle\ApiBundle\Processor\Shared\BuildCriteria - arguments: - - '@oro_api.config_provider' - - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: delete_list, group: build_query, priority: 50 } diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_list.yml b/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_list.yml index 29d1c381e69..25d56997973 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_list.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_list.yml @@ -56,10 +56,11 @@ services: tags: - { name: oro.api.processor, action: get_list, group: initialize, requestType: json_api, priority: -20 } - oro_api.get_list.register_filters: - class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterFilters + oro_api.get_list.register_configured_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterConfiguredFilters arguments: - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: get_list, group: initialize, priority: -50 } @@ -70,6 +71,15 @@ services: tags: - { name: oro.api.processor, action: get_list, group: initialize, requestType: json_api, priority: -55 } + oro_api.get_list.register_dynamic_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterDynamicFilters + arguments: + - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' + - '@oro_api.config_provider' + tags: + - { name: oro.api.processor, action: get_list, group: initialize, priority: -60 } + oro_api.get_list.rest.set_default_sorting: class: Oro\Bundle\ApiBundle\Processor\Shared\Rest\SetDefaultSorting arguments: @@ -165,9 +175,6 @@ services: oro_api.get_list.build_criteria: class: Oro\Bundle\ApiBundle\Processor\Shared\BuildCriteria - arguments: - - '@oro_api.config_provider' - - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: get_list, group: build_query, priority: 50 } diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_relationship.yml b/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_relationship.yml index 1e439a62b09..16ab07f4d91 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_relationship.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_relationship.yml @@ -48,10 +48,11 @@ services: tags: - { name: oro.api.processor, action: get_relationship, group: initialize, collection: true, priority: -10 } - oro_api.get_relationship.register_filters: - class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterFilters + oro_api.get_relationship.register_configured_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterConfiguredFilters arguments: - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: get_relationship, group: initialize, collection: true, priority: -50 } @@ -62,6 +63,15 @@ services: tags: - { name: oro.api.processor, action: get_relationship, group: initialize, collection: true, requestType: json_api, priority: -55 } + oro_api.get_relationship.register_dynamic_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterDynamicFilters + arguments: + - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' + - '@oro_api.config_provider' + tags: + - { name: oro.api.processor, action: get_relationship, group: initialize, collection: true, priority: -60 } + oro_api.get_relationship.rest.set_default_sorting: class: Oro\Bundle\ApiBundle\Processor\Shared\Rest\SetDefaultSorting arguments: @@ -157,9 +167,6 @@ services: oro_api.get_relationship.build_criteria: class: Oro\Bundle\ApiBundle\Processor\Shared\BuildCriteria - arguments: - - '@oro_api.config_provider' - - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: get_relationship, group: build_query, collection: true, priority: 50 } diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_subresource.yml b/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_subresource.yml index 0abaef30e67..398dd4a0ce6 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_subresource.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/processors.get_subresource.yml @@ -62,10 +62,11 @@ services: tags: - { name: oro.api.processor, action: get_subresource, group: initialize, requestType: json_api, priority: -20 } - oro_api.get_subresource.register_filters: - class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterFilters + oro_api.get_subresource.register_configured_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterConfiguredFilters arguments: - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: get_subresource, group: initialize, collection: true, priority: -50 } @@ -76,6 +77,15 @@ services: tags: - { name: oro.api.processor, action: get_subresource, group: initialize, collection: true, requestType: json_api, priority: -55 } + oro_api.get_subresource.register_dynamic_filters: + class: Oro\Bundle\ApiBundle\Processor\Shared\RegisterDynamicFilters + arguments: + - '@oro_api.filter_factory' + - '@oro_api.doctrine_helper' + - '@oro_api.config_provider' + tags: + - { name: oro.api.processor, action: get_subresource, group: initialize, collection: true, priority: -60 } + oro_api.get_subresource.rest.set_default_sorting: class: Oro\Bundle\ApiBundle\Processor\Shared\Rest\SetDefaultSorting arguments: @@ -183,9 +193,6 @@ services: oro_api.get_subresource.build_criteria: class: Oro\Bundle\ApiBundle\Processor\Shared\BuildCriteria - arguments: - - '@oro_api.config_provider' - - '@oro_api.doctrine_helper' tags: - { name: oro.api.processor, action: get_subresource, group: build_query, collection: true, priority: 50 } diff --git a/src/Oro/Bundle/ApiBundle/Resources/config/services.yml b/src/Oro/Bundle/ApiBundle/Resources/config/services.yml index 69c44cfa606..54149da80a7 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/ApiBundle/Resources/config/services.yml @@ -1,9 +1,3 @@ -parameters: - # the maximum number of nesting target entities that can be specified in 'Resources/config/oro/api.yml' - oro_api.config.max_nesting_level: 3 - # all supported ApiDoc views - oro_api.api_doc.views: ['default', 'rest_json_api', 'rest_plain'] - services: oro_api.action_processor_bag: class: Oro\Bundle\ApiBundle\Processor\ActionProcessorBag @@ -100,6 +94,11 @@ services: - '@oro_api.doctrine_helper' - '@oro_api.entity_accessor' - '@oro_api.entity_serializer.data_transformer' + - '@oro_api.object_normalizer.config_normalizer' + + oro_api.object_normalizer.config_normalizer: + class: Oro\Bundle\ApiBundle\Normalizer\ConfigNormalizer + public: false oro_api.object_normalizer_registry: class: Oro\Bundle\ApiBundle\Normalizer\ObjectNormalizerRegistry @@ -123,6 +122,13 @@ services: - '@oro_api.processor_bag' - customize_loaded_data + oro_api.customize_form_data.processor: + class: Oro\Bundle\ApiBundle\Processor\CustomizeFormDataProcessor + public: false + arguments: + - '@oro_api.processor_bag' + - customize_form_data + oro_api.resources_provider: class: Oro\Bundle\ApiBundle\Provider\ResourcesProvider arguments: @@ -159,7 +165,7 @@ services: oro_api.config_extension_registry: class: Oro\Bundle\ApiBundle\Config\ConfigExtensionRegistry arguments: - - %oro_api.config.max_nesting_level% + - 0 # config_max_nesting_level; it is set by Oro\Bundle\ApiBundle\DependencyInjection\OroApiExtension oro_api.config_extension.filters: class: Oro\Bundle\ApiBundle\Config\FiltersConfigExtension @@ -335,6 +341,7 @@ services: - '\OutOfBoundsException' - 'Oro\Bundle\ApiBundle\Exception\ExceptionInterface' - 'Symfony\Component\HttpKernel\Exception\HttpExceptionInterface' + - 'Symfony\Component\PropertyAccess\Exception\ExceptionInterface' tags: - { name: oro.api.exception_text_extractor, priority: -10 } diff --git a/src/Oro/Bundle/ApiBundle/Resources/doc/actions.md b/src/Oro/Bundle/ApiBundle/Resources/doc/actions.md index a08099c513f..347cc8719c0 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/doc/actions.md +++ b/src/Oro/Bundle/ApiBundle/Resources/doc/actions.md @@ -17,6 +17,7 @@ Table of Contents - [**add_relationship** action](#add_relationship-action) - [**delete_relationship** action](#delete_relationship-action) - [**customize_loaded_data** action](#customize_loaded_data-action) + - [**customize_form_data** action](#customize_form_data-action) - [**get_config** action](#get_config-action) - [**get_relation_config** action](#get_relation_config-action) - [**get_metadata** action](#get_metadata-action) @@ -55,6 +56,7 @@ The following table shows all actions provided out of the box: | [add_relationship](#add_relationship-action) | Adds one or several entities to a relationship. This action is applicable only for "to-many" relationships | | [delete_relationship](#delete_relationship-action) | Deletes one or several entities from a relationship. This action is applicable only for "to-many" relationships | | [customize_loaded_data](#customize_loaded_data-action) | Makes modifications of data loaded by [get](#get-action), [get_list](#get_list-action) and [get_subresource](#get_subresource-action) actions | +| [customize_form_data](#customize_form_data-action) | Makes modifications of submitted form data for [create](#create-action) and [update](#update-action) actions | | [get_config](#get_config-action) | Returns a configuration of an entity | | [get_relation_config](#get_relation_config-action) | Returns a configuration of an entity if it is used in a relationship | | [get_metadata](#get_metadata-action) | Returns a metadata of an entity | @@ -170,7 +172,7 @@ This action is intended to delete a list of entities. The entities list is built based on input filters. Please take into account that at least one filter must be specified, otherwise an error raises. By default the maximum number of entities that can be deleted by one request is 100. This limit was introduced to minimize impact on the server. -You can change this limit for an entity in `Resources/config/acl.yml`, but please test your limit carefully because a big limit may make a big impact to the server. +You can change this limit for an entity in `Resources/config/oro/api.yml`, but please test your limit carefully because a big limit may make a big impact to the server. An example how to change default limit you can read at [how-to](how_to.md#change-the-maximum-number-of-entities-that-can-be-deleted-by-one-request). The route name for REST API: `oro_rest_api_cdelete`. @@ -431,113 +433,22 @@ customize_loaded_data Action This action is intended to make modifications of data loaded by [get](#get-action), [get_list](#get_list-action) and [get_subresource](#get_subresource-action) actions. -The context class: [CustomizeLoadedDataContext](../../Processor/CollectResources/CustomizeLoadedDataContext.php). +The context class: [CustomizeLoadedDataContext](../../Processor/CustomizeLoadedData/CustomizeLoadedDataContext.php). The main processor class: [CustomizeLoadedDataProcessor](../../Processor/CustomizeLoadedDataProcessor.php). -There are no worker processors in ApiBundle. To see existing worker processors from other bundles run `php app/console oro:api:debug customize_loaded_data`. +As example of a processor is used to modify loaded data you can see [ComputePrimaryField](../../Processor/CustomizeLoadedData/ComputePrimaryField.php). Also you can run `php app/console oro:api:debug customize_loaded_data` to see other processors registered in this action. -An example of a processor to modify loaded data: - -```php -fileManager = $fileManager; - $this->logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function process(ContextInterface $context) - { - /** @var CustomizeLoadedDataContext $context */ - - $data = $context->getResult(); - if (!is_array($data)) { - return; - } - - $config = $context->getConfig(); - $contentField = $config->getField('content'); - if (!$contentField || $contentField->isExcluded()) { - return; - } - - if (empty($data['filename'])) { - return; - } - - $content = $this->getFileContent($data['filename']); - if (null !== $content) { - $data[$contentField->getPropertyPath()] = $content; - $context->setResult($data); - } - } - - /** - * @param string $fileName - * - * @return string|null - */ - protected function getFileContent($fileName) - { - $content = null; - try { - $content = $this->fileManager->getContent($fileName); - } catch (FileNotFound $e) { - $this->logger->error( - sprintf('The content for "%s" file cannot be loaded.', $fileName), - ['exception' => $e] - ); - } - if (null !== $content) { - $content = base64_encode($content); - } - - return $content; - } -} -``` - -```yaml - oro_attachment.api.customize_loaded_data.compute_file_content: - class: Oro\Bundle\AttachmentBundle\Api\Processor\ComputeFileContent - arguments: - - '@oro_attachment.manager' - - '@logger' - tags: - - { name: oro.api.processor, action: customize_loaded_data, class: Oro\Bundle\AttachmentBundle\Entity\File } - - { name: monolog.logger, channel: api } -``` +As example of a processor is used to modify loaded data you can see [MapPrimaryField](../../Processor/CustomizeFormData/MapPrimaryField.php). Also you can run `php app/console oro:api:debug customize_form_data` to see other processors registered in this action. get_config Action ----------------- @@ -642,8 +553,11 @@ Example of usage: /** @var ResourcesProvider $resourcesProvider */ $resourcesProvider = $container->get('oro_api.resources_provider'); // get all Data API resources +// (all resources are configured to be used in Data API, including not accessible resources) $resources = $resourcesProvider->getResources($version, $requestType); -// check whether an entity type is accessible through Data API +// check whether an entity is configured to be used in Data API +$isKnown = $resourcesProvider->isResourceKnown($entityClass, $version, $requestType); +// check whether an entity is accessible through Data API $isAccessible = $resourcesProvider->isResourceAccessible($entityClass, $version, $requestType); ``` diff --git a/src/Oro/Bundle/ApiBundle/Resources/doc/configuration.md b/src/Oro/Bundle/ApiBundle/Resources/doc/configuration.md index b05df87ca8d..8fc4ceae49b 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/doc/configuration.md +++ b/src/Oro/Bundle/ApiBundle/Resources/doc/configuration.md @@ -50,12 +50,13 @@ By default this command shows configuration of nesting entities. To simplify the php app/console oro:api:config:dump-reference --max-nesting-level=0 ``` -The default nesting level is `3`. It is specified in [services.yml](../config/services.yml) via the `oro_api.config.max_nesting_level` parameter. So, if needed, you can easily change this value. +The default nesting level is `3`. It is specified in the configuration of ApiBundle via the `config_max_nesting_level` parameter. So, if needed, you can easily change this value, for example: ```yaml -parameters: - # the maximum number of nesting target entities that can be specified in 'Resources/config/oro/api.yml' - oro_api.config.max_nesting_level: 3 +# app/config/config.yml + +oro_api: + config_max_nesting_level: 3 ``` The first level sections of configuration are: @@ -185,7 +186,6 @@ The `entities` section describes a configuration of entities. * **disable_fieldset** *boolean* The flag indicates whether a requesting of a restricted set of fields is disabled. In JSON.API an [**fields** request parameter](http://jsonapi.org/format/#fetching-sparse-fieldsets) can be used to customize which fields should be returned. By default `false`. * **hints** *array* Sets [Doctrine query hints](http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#query-hints). Each item can be a string or an array with `name` and `value` keys. The string value is a short form of `[name: hint name]`. * **identifier_field_names** *string[]* The names of identifier fields of the entity. Usually it should be set in a configuration file in case if Data API resource is based on not ORM entity. For ORM entities a value of this option is retrieved from an entity metadata. -* **post_serialize** *callable* A handler to be used to modify serialized data. * **delete_handler** *string* The id of a service that should be used to delete entity by the [delete](./actions.md#delete-action) and [delete_list](./actions.md#delete_list-action) actions. By default the [oro_soap.handler.delete](../../../SoapBundle/Handler/DeleteHandler.php) service is used. * **form_type** *string* The form type that should be used for the entity in [create](./actions.md#create-action) and [update](./actions.md#update-action) actions. By default the `form` form type is used. * **form_options** *array* The form options that should be used for the entity in [create](./actions.md#create-action) and [update](./actions.md#update-action) actions. @@ -214,7 +214,6 @@ oro_api: - HINT_TRANSLATABLE - { name: HINT_FILTER_BY_CURRENT_USER } - { name: HINT_CUSTOM_OUTPUT_WALKER, value: "Acme\Bundle\AcmeBundle\AST_Walker_Class"} - post_serialize: ["Acme\Bundle\AcmeBundle\Serializer\MySerializationHandler", "serialize"] delete_handler: acme.demo.test_entity.delete_handler excluded: false form_type: acme_entity.api_form @@ -238,7 +237,7 @@ This section describes entity fields' configuration. * **meta_property** *boolean* A flag indicates whether the field represents a meta information. For JSON.API such fields will be returned in [meta](http://jsonapi.org/format/#document-meta) section. By default `false`. * **target_class** *string* The class name of a target entity if a field represents an association. Usually it should be set in a configuration file in case if Data API resource is based on not ORM entity. * **target_type** *string* The type of a target association. Can be **to-one** or **to-many**. Also **collection** can be used as an alias for **to-many**. **to-one** can be omitted as it is used by default. Usually it should be set in a configuration file in case if Data API resource is based on not ORM entity. -* **depends_on** *string[]* A list of fields on which this field depends on. This option can be helpful for computed fields. These fields will be loaded from the database even if they are excluded. +* **depends_on** *string[]* A list of fields on which this field depends on. Also `.` can be used to specify a path to an association field. This option can be helpful for computed fields. These fields will be loaded from the database even if they are excluded. Examples: @@ -301,7 +300,7 @@ oro_api: # computed field field9: data_type: string - depends_on: [field1] + depends_on: [field1, association1.field11] ``` "filters" configuration section @@ -316,6 +315,9 @@ This section describes fields by which the result data can be filtered. It conta * **property_path** *string* The property path to reach the fields' value. The same way as above in `fields` configuration section. * **data_type** *string* The data type of the filter value. Can be `boolean`, `integer`, `string`, etc. * **allow_array** *boolean* A flag indicates whether the filter can contains several values. By default `false`. + * **type** *string* The filter type. By default the filter type is equal to the **data_type** property. + * **options** *array* The filter options. + * **operators** *array* A list of operators supported by the filter. By default the list of operators depends on the filter type. For example a string filter supports **=** and **!=** operators, a number filter supports **=**, **!=**, **<**, **<=**, **>** and **>=** operators, etc. Usually you need to use this parameter in case if you need to make a list of supported operators more limited. Example: @@ -336,6 +338,13 @@ oro_api: field3: data_type: boolean allow_array: false + field4: + data_type: string + type: myFilter + options: + my_option: value + field5: + operators: ['='] ``` "sorters" configuration section @@ -401,6 +410,16 @@ By default, the following permissions are used to restrict access to an entity i Examples of `actions` section configuration: +Disable all action for an entity: + +```yaml +api: + entities: + Acme\Bundle\AcmeBundle\Entity\AcmeEntity: + # this entity does not have own Data API resource + actions: false +``` + Disable `delete` action for an entity: ```yaml diff --git a/src/Oro/Bundle/ApiBundle/Resources/doc/how_to.md b/src/Oro/Bundle/ApiBundle/Resources/doc/how_to.md index 451c33d2999..7e8fedb64b5 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/doc/how_to.md +++ b/src/Oro/Bundle/ApiBundle/Resources/doc/how_to.md @@ -93,7 +93,7 @@ As result, the `VIEW` permission will be used instead of `DELETE` permission. Disable access checks for action -------------------------------- -You can disable access checks for some action by setting `null` as a value to `acl_resource` option in `Resources/config/acl.yml`: +You can disable access checks for some action by setting `null` as a value to `acl_resource` option in `Resources/config/oro/api.yml`: ```yaml oro_api: @@ -109,7 +109,7 @@ Disable entity action When you add an entity to the API, all the actions will be available by default. -In case if an action should not be accessible, you can disable it in `Resources/config/acl.yml`: +In case if an action should not be accessible, you can disable it in `Resources/config/oro/api.yml`: ```yaml oro_api: @@ -135,7 +135,7 @@ Change delete handler for entity By default, entity deletion is processed by [DeleteHandler](../../../SoapBundle/Handler/DeleteHandler.php). -If your want to use another delete handler, you can set it by the `delete_handler` option in `Resources/config/acl.yml`: +If your want to use another delete handler, you can set it by the `delete_handler` option in `Resources/config/oro/api.yml`: ```yaml oro_api: @@ -153,7 +153,7 @@ Change the maximum number of entities that can be deleted by one request By default, the [delete_list](./actions.md#delete_list-action) action can delete not more than 100 entities. This limit is set by the [SetDeleteLimit](../../Processor/DeleteList/SetDeleteLimit.php) processor. -If your want to use another limit, you can set it by the `max_results` option in `Resources/config/acl.yml`: +If your want to use another limit, you can set it by the `max_results` option in `Resources/config/oro/api.yml`: ```yaml oro_api: diff --git a/src/Oro/Bundle/ApiBundle/Resources/doc/index.md b/src/Oro/Bundle/ApiBundle/Resources/doc/index.md index 8365bffd9f8..3185ce4589d 100644 --- a/src/Oro/Bundle/ApiBundle/Resources/doc/index.md +++ b/src/Oro/Bundle/ApiBundle/Resources/doc/index.md @@ -29,6 +29,7 @@ OroApiBundle Documentation - [**add_relationship** action](./actions.md#add_relationship-action) - [**delete_relationship** action](./actions.md#delete_relationship-action) - [**customize_loaded_data** action](./actions.md#customize_loaded_data-action) + - [**customize_form_data** action](./actions.md#customize_form_data-action) - [**get_config** action](./actions.md#get_config-action) - [**get_relation_config** action](./actions.md#get_relation_config-action) - [**get_metadata** action](./actions.md#get_metadata-action) diff --git a/src/Oro/Bundle/ApiBundle/Tests/Functional/ApiTestCase.php b/src/Oro/Bundle/ApiBundle/Tests/Functional/ApiTestCase.php index 2f455450a79..36c6805ebfb 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Functional/ApiTestCase.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Functional/ApiTestCase.php @@ -195,20 +195,24 @@ protected function loadExpectation($filename) } /** - * @param Response $response - * @param integer $statusCode - * @param string $entityName - * @param string $requestType + * @param Response $response + * @param int|int[] $statusCode + * @param string $entityName + * @param string $requestType */ - protected function assertApiResponseStatusCodeEquals(Response $response, $statusCode, $entityName, $requestType) - { + protected static function assertApiResponseStatusCodeEquals( + Response $response, + $statusCode, + $entityName, + $requestType + ) { try { - $this->assertResponseStatusCodeEquals($response, $statusCode); + static::assertResponseStatusCodeEquals($response, $statusCode); } catch (\PHPUnit_Framework_ExpectationFailedException $e) { $e = new \PHPUnit_Framework_ExpectationFailedException( sprintf( 'Expects %s status code for "%s" request for entity: "%s". Error message: %s', - $statusCode, + is_array($statusCode) ? implode(', ', $statusCode) : $statusCode, $requestType, $entityName, $e->getMessage() @@ -221,12 +225,12 @@ protected function assertApiResponseStatusCodeEquals(Response $response, $status /** * @param Response $response - * @param integer $statusCode + * @param int|int[] $statusCode * @param string $entityName * @param string $requestType * @param array|null $content */ - protected function assertUpdateApiResponseStatusCodeEquals( + protected static function assertUpdateApiResponseStatusCodeEquals( Response $response, $statusCode, $entityName, @@ -234,12 +238,12 @@ protected function assertUpdateApiResponseStatusCodeEquals( $content ) { try { - $this->assertResponseStatusCodeEquals($response, $statusCode); + static::assertResponseStatusCodeEquals($response, $statusCode); } catch (\PHPUnit_Framework_ExpectationFailedException $e) { $e = new \PHPUnit_Framework_ExpectationFailedException( sprintf( 'Expects %s status code for "%s" request for entity: "%s". Error message: %s. Content: %s', - $statusCode, + is_array($statusCode) ? implode(', ', $statusCode) : $statusCode, $requestType, $entityName, $e->getMessage(), @@ -254,15 +258,27 @@ protected function assertUpdateApiResponseStatusCodeEquals( /** * Assert response status code equals * - * @param Response $response - * @param int $statusCode + * @param Response $response + * @param int|int[] $statusCode */ public static function assertResponseStatusCodeEquals(Response $response, $statusCode) { try { - \PHPUnit_Framework_TestCase::assertEquals($statusCode, $response->getStatusCode()); + if (is_array($statusCode)) { + if (!in_array($response->getStatusCode(), $statusCode, true)) { + throw new \PHPUnit_Framework_ExpectationFailedException( + sprintf( + 'Failed asserting that %s is one of %s', + $response->getStatusCode(), + implode(', ', $statusCode) + ) + ); + } + } else { + \PHPUnit_Framework_TestCase::assertEquals($statusCode, $response->getStatusCode()); + } } catch (\PHPUnit_Framework_ExpectationFailedException $e) { - if ($statusCode < 400 + if ((is_array($statusCode) ? min($statusCode) : $statusCode) <= 400 && $response->getStatusCode() >= 400 && ( $response->headers->contains('Content-Type', 'application/json') @@ -270,7 +286,7 @@ public static function assertResponseStatusCodeEquals(Response $response, $statu ) ) { $e = new \PHPUnit_Framework_ExpectationFailedException( - $e->getMessage() . ' Response content: ' . $response->getContent(), + $e->getMessage() . "\nResponse content: " . $response->getContent(), $e->getComparisonFailure() ); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Functional/RestJsonApiFormValidationTest.php b/src/Oro/Bundle/ApiBundle/Tests/Functional/RestJsonApiFormValidationTest.php new file mode 100644 index 00000000000..ec6e7843ed8 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Functional/RestJsonApiFormValidationTest.php @@ -0,0 +1,56 @@ +getEntityType($entityClass); + + $response = $this->request( + 'POST', + $this->getUrl('oro_rest_api_post', ['entity' => $entityType]), + ['data' => ['type' => $entityType, 'attributes' => ['notExistingField' => null]]] + ); + $this->assertApiResponseStatusCodeEquals($response, [400, 403], $entityType, 'post'); + } + + /** + * @param string $entityClass + * @param string[] $excludedActions + * + * @dataProvider getEntities + */ + public function testUpdateRequests($entityClass, $excludedActions) + { + if (in_array(ApiActions::UPDATE, $excludedActions, true)) { + return; + } + + $entityType = $this->getEntityType($entityClass); + + $response = $this->request( + 'PATCH', + $this->getUrl('oro_rest_api_patch', ['entity' => $entityType, 'id' => '1']), + ['data' => ['type' => $entityType, 'id' => '1', 'attributes' => ['notExistingField' => null]]] + ); + $this->assertApiResponseStatusCodeEquals($response, [400, 403], $entityType, 'post'); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/ActionFieldConfigTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/ActionFieldConfigTest.php index f56eab4e1c3..22af3a323bf 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/ActionFieldConfigTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/ActionFieldConfigTest.php @@ -71,10 +71,12 @@ public function testPropertyPath() $config = new ActionFieldConfig(); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $config->setPropertyPath('path'); $this->assertTrue($config->hasPropertyPath()); $this->assertEquals('path', $config->getPropertyPath()); + $this->assertEquals('path', $config->getPropertyPath('default')); $this->assertEquals(['property_path' => 'path'], $config->toArray()); $config->setPropertyPath(null); @@ -86,6 +88,7 @@ public function testPropertyPath() $config->setPropertyPath(''); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $this->assertEquals([], $config->toArray()); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/actions.yml b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/actions.yml index de9de41a4d0..5107cd54d9b 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/actions.yml +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/actions.yml @@ -12,6 +12,34 @@ null_actions: actions: ~ expected: [] +exclude_all_actions: + config: + actions: false + expected: + actions: + get: + exclude: true + get_list: + exclude: true + update: + exclude: true + create: + exclude: true + delete: + exclude: true + delete_list: + exclude: true + get_subresource: + exclude: true + get_relationship: + exclude: true + update_relationship: + exclude: true + add_relationship: + exclude: true + delete_relationship: + exclude: true + description: config: actions: diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/filters.yml b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/filters.yml index fcb39faef9a..7bf1f85bbeb 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/filters.yml +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Definition/Fixtures/filters.yml @@ -146,3 +146,130 @@ empty_field_description: description: "" expected: [] error: 'The path "entity.filters.fields.field1.description" cannot contain an empty value, but got "".' + +null_field_type: + config: + filters: + fields: + field1: + type: ~ + expected: [] + error: 'The path "entity.filters.fields.field1.type" cannot contain an empty value, but got null.' + +empty_field_type: + config: + filters: + fields: + field1: + type: "" + expected: [] + error: 'The path "entity.filters.fields.field1.type" cannot contain an empty value, but got "".' + +invalid_field_type: + config: + filters: + fields: + field1: + type: [] + expected: [] + error: 'Invalid type for path "entity.filters.fields.field1.type". Expected scalar, but got array.' + +field_type: + config: + filters: + fields: + field1: + type: integer + expected: + filters: + fields: + field1: + type: integer + +null_field_options: + config: + filters: + fields: + field1: + options: ~ + expected: + filters: + fields: + field1: [] + +empty_field_options: + config: + filters: + fields: + field1: + options: [] + expected: + filters: + fields: + field1: [] + +invalid_field_options: + config: + filters: + fields: + field1: + options: test + expected: [] + error: 'Invalid type for path "entity.filters.fields.field1.options". Expected array, but got string' + +field_options: + config: + filters: + fields: + field1: + options: + option1: value1 + expected: + filters: + fields: + field1: + options: + option1: value1 + +null_field_operators: + config: + filters: + fields: + field1: + operators: ~ + expected: + filters: + fields: + field1: [] + +empty_field_operators: + config: + filters: + fields: + field1: + operators: [] + expected: + filters: + fields: + field1: [] + +invalid_field_operators: + config: + filters: + fields: + field1: + operators: test + expected: [] + error: 'Invalid type for path "entity.filters.fields.field1.operators". Expected array, but got string' + +field_operators: + config: + filters: + fields: + field1: + operators: ['=', '!='] + expected: + filters: + fields: + field1: + operators: ['=', '!='] diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/EntityDefinitionFieldConfigTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/EntityDefinitionFieldConfigTest.php index 8161aa26973..51cd05c0372 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/EntityDefinitionFieldConfigTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/EntityDefinitionFieldConfigTest.php @@ -55,10 +55,12 @@ public function testPropertyPath() $config = new EntityDefinitionFieldConfig(); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $config->setPropertyPath('path'); $this->assertTrue($config->hasPropertyPath()); $this->assertEquals('path', $config->getPropertyPath()); + $this->assertEquals('path', $config->getPropertyPath('default')); $this->assertEquals(['property_path' => 'path'], $config->toArray()); $config->setPropertyPath(null); @@ -70,6 +72,7 @@ public function testPropertyPath() $config->setPropertyPath(''); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $this->assertEquals([], $config->toArray()); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/FilterFieldConfigTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/FilterFieldConfigTest.php index 83b100a7cbb..b49ec6adb74 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/FilterFieldConfigTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/FilterFieldConfigTest.php @@ -52,10 +52,12 @@ public function testPropertyPath() $config = new FilterFieldConfig(); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $config->setPropertyPath('path'); $this->assertTrue($config->hasPropertyPath()); $this->assertEquals('path', $config->getPropertyPath()); + $this->assertEquals('path', $config->getPropertyPath('default')); $this->assertEquals(['property_path' => 'path'], $config->toArray()); $config->setPropertyPath(null); @@ -67,6 +69,7 @@ public function testPropertyPath() $config->setPropertyPath(''); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $this->assertEquals([], $config->toArray()); } @@ -132,4 +135,46 @@ public function testArrayAllowed() $this->assertFalse($config->isArrayAllowed()); $this->assertEquals([], $config->toArray()); } + + public function testType() + { + $config = new FilterFieldConfig(); + $this->assertNull($config->getType()); + + $config->setType('test'); + $this->assertEquals('test', $config->getType()); + $this->assertEquals(['type' => 'test'], $config->toArray()); + + $config->setType(null); + $this->assertNull($config->getType()); + $this->assertEquals([], $config->toArray()); + } + + public function testOptions() + { + $config = new FilterFieldConfig(); + $this->assertNull($config->getOptions()); + + $config->setOptions(['key' => 'val']); + $this->assertEquals(['key' => 'val'], $config->getOptions()); + $this->assertEquals(['options' => ['key' => 'val']], $config->toArray()); + + $config->setOptions(null); + $this->assertNull($config->getOptions()); + $this->assertEquals([], $config->toArray()); + } + + public function testOperators() + { + $config = new FilterFieldConfig(); + $this->assertNull($config->getOperators()); + + $config->setOperators(['=', '!=']); + $this->assertEquals(['=', '!='], $config->getOperators()); + $this->assertEquals(['operators' => ['=', '!=']], $config->toArray()); + + $config->setOperators(null); + $this->assertNull($config->getOperators()); + $this->assertEquals([], $config->toArray()); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Fixtures/Loader/filters.yml b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Fixtures/Loader/filters.yml index 4c536eb3cc7..291954a9620 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Fixtures/Loader/filters.yml +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/Fixtures/Loader/filters.yml @@ -88,3 +88,89 @@ full: field17: ~ field18: ~ extra1: true + +null_field_type: + config: + fields: + field1: + type: ~ + expected: + fields: + field1: ~ + +empty_field_type: + config: + fields: + field1: + type: "" + expected: + fields: + field1: ~ + +field_type: + config: + fields: + field1: + type: integer + expected: + fields: + field1: + type: integer + +null_field_options: + config: + fields: + field1: + options: ~ + expected: + fields: + field1: ~ + +empty_field_options: + config: + fields: + field1: + options: [] + expected: + fields: + field1: ~ + +field_options: + config: + fields: + field1: + options: + option1: value1 + expected: + fields: + field1: + options: + option1: value1 + +null_field_operators: + config: + fields: + field1: + operators: ~ + expected: + fields: + field1: ~ + +empty_field_operators: + config: + fields: + field1: + operators: [] + expected: + fields: + field1: ~ + +field_operators: + config: + fields: + field1: + operators: ['=', '!='] + expected: + fields: + field1: + operators: ['=', '!='] diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/SorterFieldConfigTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/SorterFieldConfigTest.php index 1d94b91702a..1c6a5d492ba 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/SorterFieldConfigTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Config/SorterFieldConfigTest.php @@ -52,10 +52,12 @@ public function testPropertyPath() $config = new SorterFieldConfig(); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $config->setPropertyPath('path'); $this->assertTrue($config->hasPropertyPath()); $this->assertEquals('path', $config->getPropertyPath()); + $this->assertEquals('path', $config->getPropertyPath('default')); $this->assertEquals(['property_path' => 'path'], $config->toArray()); $config->setPropertyPath(null); @@ -67,6 +69,7 @@ public function testPropertyPath() $config->setPropertyPath(''); $this->assertFalse($config->hasPropertyPath()); $this->assertNull($config->getPropertyPath()); + $this->assertEquals('default', $config->getPropertyPath('default')); $this->assertEquals([], $config->toArray()); } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ChainFilterFactoryTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ChainFilterFactoryTest.php index 86f7bdd5573..31ee5888e82 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ChainFilterFactoryTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ChainFilterFactoryTest.php @@ -24,24 +24,24 @@ public function testChainFactory() ->method('createFilter') ->willReturnMap( [ - ['known1', $knownFilter1], - ['known3', $knownFilter31], - ['unknown1', null], + ['known1', [], $knownFilter1], + ['known3', ['some_option' => 'val'], $knownFilter31], + ['unknown1', [], null], ] ); $childFactory2->expects($this->any()) ->method('createFilter') ->willReturnMap( [ - ['known2', $knownFilter2], - ['known3', $knownFilter32], - ['unknown2', null], + ['known2', [], $knownFilter2], + ['known3', ['some_option' => 'val'], $knownFilter32], + ['unknown2', [], null], ] ); $this->assertSame($knownFilter1, $chainFactory->createFilter('known1')); $this->assertSame($knownFilter2, $chainFactory->createFilter('known2')); - $this->assertSame($knownFilter31, $chainFactory->createFilter('known3')); + $this->assertSame($knownFilter31, $chainFactory->createFilter('known3', ['some_option' => 'val'])); $this->assertNull($chainFactory->createFilter('unknown1')); $this->assertNull($chainFactory->createFilter('unknown2')); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ComparisonFilterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ComparisonFilterTest.php index 6c33a9e1830..053d185582c 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ComparisonFilterTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/ComparisonFilterTest.php @@ -34,7 +34,7 @@ protected function setUp() /** * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Field must not be empty + * @expectedExceptionMessage The Field must not be empty. */ public function testInvalidArgumentExceptionField() { @@ -83,8 +83,8 @@ public function testFilterWhenOperatorsAreNotSpecified() $comparisonFilter->apply($criteria, new FilterValue('path', 'value', ComparisonFilter::EQ)); $this->assertEquals( - new Criteria(new Comparison('fieldName', Comparison::EQ, 'value')), - $criteria + new Comparison('fieldName', Comparison::EQ, 'value'), + $criteria->getWhereExpression() ); } @@ -100,8 +100,8 @@ public function testFilterWhenOnlyEqualOperatorIsSpecified() $comparisonFilter->apply($criteria, new FilterValue('path', 'value', ComparisonFilter::EQ)); $this->assertEquals( - new Criteria(new Comparison('fieldName', Comparison::EQ, 'value')), - $criteria + new Comparison('fieldName', Comparison::EQ, 'value'), + $criteria->getWhereExpression() ); } @@ -129,7 +129,7 @@ public function testFilter($fieldName, $isArrayAllowed, $filterValue, $expectati $criteria = new Criteria(); $this->comparisonFilter->apply($criteria, $filterValue); - $this->assertEquals($expectation, $criteria); + $this->assertEquals($expectation, $criteria->getWhereExpression()); } public function testCaseProvider() @@ -139,49 +139,49 @@ public function testCaseProvider() 'fieldName', //fieldName true, //isArrayAllowed null, //filter - new Criteria() //expectation + null //expectation ], 'filter with default operator' => [ 'fieldName', true, new FilterValue('path', 'value'), - new Criteria(new Comparison('fieldName', Comparison::EQ, 'value')) + new Comparison('fieldName', Comparison::EQ, 'value') ], 'EQ filter' => [ 'fieldName', true, new FilterValue('path', 'value', ComparisonFilter::EQ), - new Criteria(new Comparison('fieldName', Comparison::EQ, 'value')) + new Comparison('fieldName', Comparison::EQ, 'value') ], 'NEQ filter' => [ 'fieldName', true, new FilterValue('path', 'value', ComparisonFilter::NEQ), - new Criteria(new Comparison('fieldName', Comparison::NEQ, 'value')) + new Comparison('fieldName', Comparison::NEQ, 'value') ], 'LT filter' => [ 'fieldName', false, new FilterValue('path', 'value', ComparisonFilter::LT), - new Criteria(new Comparison('fieldName', Comparison::LT, 'value')) + new Comparison('fieldName', Comparison::LT, 'value') ], 'LTE filter' => [ 'fieldName', false, new FilterValue('path', 'value', ComparisonFilter::LTE), - new Criteria(new Comparison('fieldName', Comparison::LTE, 'value')) + new Comparison('fieldName', Comparison::LTE, 'value') ], 'GT filter' => [ 'fieldName', false, new FilterValue('path', 'value', ComparisonFilter::GT), - new Criteria(new Comparison('fieldName', Comparison::GT, 'value')) + new Comparison('fieldName', Comparison::GT, 'value') ], 'GTE filter' => [ 'fieldName', false, new FilterValue('path', 'value', ComparisonFilter::GTE), - new Criteria(new Comparison('fieldName', Comparison::GTE, 'value')) + new Comparison('fieldName', Comparison::GTE, 'value') ] ]; } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/FieldsFilterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/FieldsFilterTest.php deleted file mode 100644 index 88a05e514fe..00000000000 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/FieldsFilterTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertNull($filter->createExpression(null)); - $this->assertNull($filter->createExpression(new FilterValue('path', 'value', 'operator'))); - } -} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/IncludeFilterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/IncludeFilterTest.php deleted file mode 100644 index 5dbe1f535cf..00000000000 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/IncludeFilterTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertNull($filter->createExpression(null)); - $this->assertNull($filter->createExpression(new FilterValue('path', 'value', 'operator'))); - } -} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageNumberFilterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageNumberFilterTest.php index bf5fcd1672c..856b3a95dbf 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageNumberFilterTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageNumberFilterTest.php @@ -10,17 +10,9 @@ class PageNumberFilterTest extends \PHPUnit_Framework_TestCase { - public function testCreateExpression() - { - $filter = new PageNumberFilter(DataType::INTEGER); - - $this->assertNull($filter->createExpression(null)); - $this->assertNull($filter->createExpression(new FilterValue('path', 'value', 'operator'))); - } - public function testApplyWithoutFilter() { - $filter = new PageNumberFilter(DataType::INTEGER); + $filter = new PageNumberFilter(DataType::INTEGER); $criteria = new Criteria(); $filter->apply($criteria); @@ -30,13 +22,13 @@ public function testApplyWithoutFilter() public function testApplyWithFilter() { - $pageSize = 10; - $pageNum = 2; + $pageSize = 10; + $pageNum = 2; $expectedOffset = 10; - $filter = new PageNumberFilter(DataType::INTEGER); + $filter = new PageNumberFilter(DataType::INTEGER); $filterValue = new FilterValue('path', $pageNum, null); - $criteria = new Criteria(); + $criteria = new Criteria(); $filter->apply($criteria, $filterValue); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageSizeFilterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageSizeFilterTest.php index 7957a352880..0668355c198 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageSizeFilterTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PageSizeFilterTest.php @@ -10,17 +10,9 @@ class PageSizeFilterTest extends \PHPUnit_Framework_TestCase { - public function testCreateExpression() - { - $filter = new PageSizeFilter(DataType::INTEGER); - - $this->assertNull($filter->createExpression(null)); - $this->assertNull($filter->createExpression(new FilterValue('name', '=', 'test'))); - } - public function testApplyWithoutFilter() { - $filter = new PageSizeFilter(DataType::INTEGER); + $filter = new PageSizeFilter(DataType::INTEGER); $criteria = new Criteria(); $filter->apply($criteria); @@ -30,9 +22,9 @@ public function testApplyWithoutFilter() public function testApplyWithFilter() { - $filter = new PageSizeFilter(DataType::INTEGER); + $filter = new PageSizeFilter(DataType::INTEGER); $filterValue = new FilterValue('path', 10, null); - $criteria = new Criteria(); + $criteria = new Criteria(); $filter->apply($criteria, $filterValue); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PrimaryFieldFilterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PrimaryFieldFilterTest.php new file mode 100644 index 00000000000..dd8324cd799 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/PrimaryFieldFilterTest.php @@ -0,0 +1,99 @@ +apply(new Criteria(), new FilterValue('path', 'value', PrimaryFieldFilter::EQ)); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The DataField must not be empty. + */ + public function testDataFieldIsNotSpecified() + { + $filter = new PrimaryFieldFilter('string'); + $filter->setField('association'); + $filter->apply(new Criteria(), new FilterValue('path', 'value', PrimaryFieldFilter::EQ)); + } + + public function testOptions() + { + $filter = new PrimaryFieldFilter('string'); + $filter->setDataField('dataField'); + $filter->setPrimaryFlagField('primaryFlagField'); + + $this->assertEquals('dataField', $filter->getDataField()); + $this->assertEquals('primaryFlagField', $filter->getPrimaryFlagField()); + } + + public function testApplyNullValue() + { + $filter = new PrimaryFieldFilter('string'); + $filter->setField('association'); + $filter->setDataField('dataField'); + + $criteria = new Criteria(); + $filter->apply($criteria, null); + + $this->assertNull($criteria->getWhereExpression()); + } + + public function testApplyWithDefaultPrimaryFlagField() + { + $filter = new PrimaryFieldFilter('string'); + $filter->setField('association'); + $filter->setDataField('dataField'); + + $criteria = new Criteria(); + $filter->apply($criteria, new FilterValue('path', 'value', PrimaryFieldFilter::EQ)); + + $this->assertEquals( + new CompositeExpression( + 'AND', + [ + new Comparison('association.dataField', Comparison::EQ, 'value'), + new Comparison('association.primary', Comparison::EQ, true), + ] + ), + $criteria->getWhereExpression() + ); + } + + public function testApplyWithCustomPrimaryFlagField() + { + $filter = new PrimaryFieldFilter('string'); + $filter->setField('association'); + $filter->setDataField('dataField'); + $filter->setPrimaryFlagField('primaryFlagField'); + + $criteria = new Criteria(); + $filter->apply($criteria, new FilterValue('path', 'value', PrimaryFieldFilter::EQ)); + + $this->assertEquals( + new CompositeExpression( + 'AND', + [ + new Comparison('association.dataField', Comparison::EQ, 'value'), + new Comparison('association.primaryFlagField', Comparison::EQ, true), + ] + ), + $criteria->getWhereExpression() + ); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SimpleFilterFactoryTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SimpleFilterFactoryTest.php index 82104bfc2c6..28cc1053269 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SimpleFilterFactoryTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SimpleFilterFactoryTest.php @@ -27,38 +27,75 @@ public function testForUnknownFilter() public function testForFilterWithoutAdditionalParameters() { - $dataType = 'string'; + $filterType = 'string'; $this->filterFactory->addFilter( - $dataType, + $filterType, 'Oro\Bundle\ApiBundle\Filter\ComparisonFilter' ); - $expectedFilter = new ComparisonFilter($dataType); + $expectedFilter = new ComparisonFilter($filterType); $this->assertEquals( $expectedFilter, - $this->filterFactory->createFilter($dataType) + $this->filterFactory->createFilter($filterType) ); } public function testForFilterWithAdditionalParameters() { - $dataType = 'string'; + $filterType = 'string'; $supportedOperators = ['=', '!=']; $this->filterFactory->addFilter( - $dataType, + $filterType, 'Oro\Bundle\ApiBundle\Filter\ComparisonFilter', ['supported_operators' => $supportedOperators] ); - $expectedFilter = new ComparisonFilter($dataType); + $expectedFilter = new ComparisonFilter($filterType); $expectedFilter->setSupportedOperators($supportedOperators); $this->assertEquals( $expectedFilter, - $this->filterFactory->createFilter($dataType) + $this->filterFactory->createFilter($filterType) + ); + } + + public function testOverrideParameters() + { + $filterType = 'string'; + + $this->filterFactory->addFilter( + $filterType, + 'Oro\Bundle\ApiBundle\Filter\ComparisonFilter', + ['supported_operators' => ['=', '!=']] + ); + + $expectedFilter = new ComparisonFilter($filterType); + $expectedFilter->setSupportedOperators(['=']); + + $this->assertEquals( + $expectedFilter, + $this->filterFactory->createFilter($filterType, ['supported_operators' => ['=']]) + ); + } + + public function testWhenFilterTypeDoesNotEqualToDataType() + { + $filterType = 'someFilter'; + $dataType = 'integer'; + + $this->filterFactory->addFilter( + $filterType, + 'Oro\Bundle\ApiBundle\Filter\ComparisonFilter' + ); + + $expectedFilter = new ComparisonFilter($dataType); + + $this->assertEquals( + $expectedFilter, + $this->filterFactory->createFilter($filterType, ['data_type' => $dataType]) ); } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SortFilterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SortFilterTest.php index e03c6c25063..4346c2767f8 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SortFilterTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Filter/SortFilterTest.php @@ -10,18 +10,9 @@ class SortFilterTest extends \PHPUnit_Framework_TestCase { - public function testCreateExpression() - { - $filter = new SortFilter(DataType::ORDER_BY); - $filterValue = new FilterValue('path', 'value', 'operator'); - - $this->assertNull($filter->createExpression(null)); - $this->assertNull($filter->createExpression($filterValue)); - } - public function testApplyWithoutFilter() { - $filter = new SortFilter(DataType::ORDER_BY); + $filter = new SortFilter(DataType::ORDER_BY); $criteria = new Criteria(); $filter->apply($criteria); @@ -33,9 +24,9 @@ public function testApplyWithFilter() { $orderingValue = ['id' => 'DESC', 'name' => 'ASC']; - $filter = new SortFilter(DataType::ORDER_BY); - $filterValue = new FilterValue('path', $orderingValue, null); - $criteria = new Criteria(); + $filter = new SortFilter(DataType::ORDER_BY); + $filterValue = new FilterValue('path', $orderingValue, null); + $criteria = new Criteria(); $filter->apply($criteria, $filterValue); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/Entity/Account.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/Entity/Account.php new file mode 100644 index 00000000000..231ad96d3a6 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/Entity/Account.php @@ -0,0 +1,100 @@ +roles = new ArrayCollection(); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param int $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return Role[]|Collection + */ + public function getRoles() + { + return $this->roles; + } + + /** + * @param Role $role + */ + public function addRole(Role $role) + { + if (!$this->roles->contains($role)) { + $this->roles->add($role); + } + } + + /** + * @param Role $role + */ + public function removeRole(Role $role) + { + if ($this->roles->contains($role)) { + $this->roles->removeElement($role); + } + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/NameContainerType.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/NameContainerType.php new file mode 100644 index 00000000000..114108fb06b --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/NameContainerType.php @@ -0,0 +1,35 @@ +add('name', 'text', $options['name_options']); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(['name_options' => []]); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'test_name_container'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/RenamedNameContainerType.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/RenamedNameContainerType.php new file mode 100644 index 00000000000..d527d898fe6 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/RenamedNameContainerType.php @@ -0,0 +1,35 @@ +add('renamedName', 'text', array_merge(['property_path' => 'name'], $options['name_options'])); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(['name_options' => []]); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'test_renamed_name_container'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/RestrictedNameContainerType.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/RestrictedNameContainerType.php new file mode 100644 index 00000000000..87e67c79ecc --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Fixtures/FormType/RestrictedNameContainerType.php @@ -0,0 +1,38 @@ + 5]); + + $builder + ->add('name', 'text', $options['name_options']); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(['name_options' => []]); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'test_restricted_name_container'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Guesser/MetadataTypeGuesserTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Guesser/MetadataTypeGuesserTest.php index 83c9313f9b7..da6668df915 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Guesser/MetadataTypeGuesserTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Guesser/MetadataTypeGuesserTest.php @@ -4,6 +4,8 @@ use Symfony\Component\Form\Guess\TypeGuess; +use Oro\Bundle\ApiBundle\Config\ConfigAccessorInterface; +use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Form\Guesser\MetadataTypeGuesser; use Oro\Bundle\ApiBundle\Metadata\AssociationMetadata; use Oro\Bundle\ApiBundle\Metadata\EntityMetadata; @@ -18,15 +20,21 @@ class MetadataTypeGuesserTest extends \PHPUnit_Framework_TestCase /** @var MetadataTypeGuesser */ protected $typeGuesser; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $doctrineHelper; + protected function setUp() { + $this->doctrineHelper = $this->getMockBuilder('Oro\Bundle\ApiBundle\Util\DoctrineHelper') + ->disableOriginalConstructor() + ->getMock(); $this->typeGuesser = new MetadataTypeGuesser( [ 'integer' => ['integer', []], 'datetime' => ['test_datetime', ['model_timezone' => 'UTC', 'view_timezone' => 'UTC']], - ] + ], + $this->doctrineHelper ); - $this->typeGuesser->setMetadataAccessor(null); } /** @@ -51,6 +59,29 @@ protected function getMetadataAccessor(EntityMetadata $metadata = null) return $metadataAccessor; } + /** + * @param string $className + * @param EntityDefinitionConfig|null $config + * + * @return ConfigAccessorInterface + */ + protected function getConfigAccessor($className, EntityDefinitionConfig $config = null) + { + $configAccessor = $this->getMock('Oro\Bundle\ApiBundle\Config\ConfigAccessorInterface'); + if (null === $config) { + $configAccessor->expects($this->once()) + ->method('getConfig') + ->willReturn(null); + } else { + $configAccessor->expects($this->once()) + ->method('getConfig') + ->with($className) + ->willReturn($config); + } + + return $configAccessor; + } + /** * @param string $fieldName * @param string $dataType @@ -204,4 +235,260 @@ public function testGuessTypeForToManyAssociation() $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) ); } + + public function testGuessTypeForArrayAssociationWithoutTargetMetadata() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $metadata->addAssociation($associationMetadata); + + $config = new EntityDefinitionConfig(); + $config->addField(self::TEST_PROPERTY); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->typeGuesser->setConfigAccessor($this->getConfigAccessor(self::TEST_CLASS, $config)); + $this->assertNull( + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } + + public function testGuessTypeForArrayAssociationForNotManageableEntity() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $metadata->addAssociation($associationMetadata); + + $targetMetadata = new EntityMetadata(); + $targetMetadata->setClassName('Test\TargetEntity'); + $associationMetadata->setTargetMetadata($targetMetadata); + + $config = new EntityDefinitionConfig(); + $associationConfig = $config->addField(self::TEST_PROPERTY)->getOrCreateTargetEntity(); + $associationConfig->addField('childField'); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with('Test\TargetEntity') + ->willReturn(false); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->typeGuesser->setConfigAccessor($this->getConfigAccessor(self::TEST_CLASS, $config)); + $this->assertEquals( + new TypeGuess( + 'oro_api_collection', + [ + 'entry_data_class' => 'Test\TargetEntity', + 'entry_type' => 'oro_api_compound_entity', + 'entry_options' => [ + 'metadata' => $targetMetadata, + 'config' => $associationConfig + ] + ], + TypeGuess::HIGH_CONFIDENCE + ), + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } + + public function testGuessTypeForArrayAssociationForManageableEntity() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $metadata->addAssociation($associationMetadata); + + $targetMetadata = new EntityMetadata(); + $targetMetadata->setClassName('Test\TargetEntity'); + $associationMetadata->setTargetMetadata($targetMetadata); + + $config = new EntityDefinitionConfig(); + $associationConfig = $config->addField(self::TEST_PROPERTY)->getOrCreateTargetEntity(); + $associationConfig->addField('childField'); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with('Test\TargetEntity') + ->willReturn(true); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->typeGuesser->setConfigAccessor($this->getConfigAccessor(self::TEST_CLASS, $config)); + $this->assertEquals( + new TypeGuess( + 'oro_api_entity_collection', + [ + 'entry_data_class' => 'Test\TargetEntity', + 'entry_type' => 'oro_api_compound_entity', + 'entry_options' => [ + 'metadata' => $targetMetadata, + 'config' => $associationConfig + ] + ], + TypeGuess::HIGH_CONFIDENCE + ), + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } + + public function testGuessTypeForCollapsedArrayAssociationWithoutTargetMetadata() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $associationMetadata->setCollapsed(); + $metadata->addAssociation($associationMetadata); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->assertNull( + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } + + public function testGuessTypeForCollapsedArrayAssociationWithoutChildFieldsAndAssociations() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $associationMetadata->setCollapsed(); + $metadata->addAssociation($associationMetadata); + + $targetMetadata = new EntityMetadata(); + $targetMetadata->setClassName('Test\TargetEntity'); + $associationMetadata->setTargetMetadata($targetMetadata); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->assertNull( + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } + + public function testGuessTypeForCollapsedArrayAssociationForNotManageableEntity() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $associationMetadata->setCollapsed(); + $metadata->addAssociation($associationMetadata); + + $targetMetadata = new EntityMetadata(); + $targetMetadata->setClassName('Test\TargetEntity'); + $targetMetadata->addField($this->createFieldMetadata('name', 'string')); + $associationMetadata->setTargetMetadata($targetMetadata); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with('Test\TargetEntity') + ->willReturn(false); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->assertEquals( + new TypeGuess( + 'oro_api_scalar_collection', + ['entry_data_class' => 'Test\TargetEntity', 'entry_data_property' => 'name'], + TypeGuess::HIGH_CONFIDENCE + ), + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } + + public function testGuessTypeForCollapsedArrayAssociationForManageableEntity() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $associationMetadata->setCollapsed(); + $metadata->addAssociation($associationMetadata); + + $targetMetadata = new EntityMetadata(); + $targetMetadata->setClassName('Test\TargetEntity'); + $targetMetadata->addField($this->createFieldMetadata('name', 'string')); + $associationMetadata->setTargetMetadata($targetMetadata); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with('Test\TargetEntity') + ->willReturn(true); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->assertEquals( + new TypeGuess( + 'oro_api_entity_scalar_collection', + ['entry_data_class' => 'Test\TargetEntity', 'entry_data_property' => 'name'], + TypeGuess::HIGH_CONFIDENCE + ), + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } + + public function testGuessTypeForCollapsedArrayAssociationWhenChildPropertyIsAssociation() + { + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS); + $associationMetadata = $this->createAssociationMetadata( + self::TEST_PROPERTY, + 'Test\TargetEntity', + true, + 'array' + ); + $associationMetadata->setCollapsed(); + $metadata->addAssociation($associationMetadata); + + $targetMetadata = new EntityMetadata(); + $targetMetadata->setClassName('Test\TargetEntity'); + $targetMetadata->addAssociation( + $this->createAssociationMetadata('association1', 'Test\TargetEntity1', false, 'integer') + ); + $associationMetadata->setTargetMetadata($targetMetadata); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with('Test\TargetEntity') + ->willReturn(true); + + $this->typeGuesser->setMetadataAccessor($this->getMetadataAccessor($metadata)); + $this->assertEquals( + new TypeGuess( + 'oro_api_entity_scalar_collection', + ['entry_data_class' => 'Test\TargetEntity', 'entry_data_property' => 'association1'], + TypeGuess::HIGH_CONFIDENCE + ), + $this->typeGuesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY) + ); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/BooleanTypeTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/BooleanTypeTest.php index b320ec56040..e4760eb9b01 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/BooleanTypeTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/BooleanTypeTest.php @@ -1,6 +1,6 @@ add( + 'name', + TextType::class, + ['constraints' => [new Assert\NotBlank()]] + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->getBlockPrefix(); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix() + { + return 'collection_entry'; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/CollectionTypeTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/CollectionTypeTest.php new file mode 100644 index 00000000000..109b7c8a3da --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/CollectionTypeTest.php @@ -0,0 +1,250 @@ + new CollectionEntryType()], + [] + ) + ]; + } + + public function testShouldUseAdder() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + $group2 = new Group(); + $group2->setName('group2'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new CollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_type' => 'collection_entry' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->once()) + ->method('addGroup') + ->with($group2); + $entity->expects($this->never()) + ->method('removeGroup'); + + $form->submit(['groups' => [['name' => 'group1'], ['name' => 'group2']]]); + $this->assertTrue($form->isSynchronized()); + } + + public function testShouldUseRemover() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + $group2 = new Group(); + $group2->setId(2); + $group2->setName('group2'); + + $entity->getGroups()->add($group1); + $entity->getGroups()->add($group2); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new CollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_type' => 'collection_entry' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->once()) + ->method('removeGroup') + ->with($this->identicalTo($group2)); + + $form->submit(['groups' => [['name' => 'group1']]]); + $this->assertTrue($form->isSynchronized()); + } + + public function testShouldUpdateExistingEntity() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new CollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_type' => 'collection_entry' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->never()) + ->method('removeGroup'); + + $form->submit(['groups' => [['name' => 'group2']]]); + $this->assertTrue($form->isSynchronized()); + + $this->assertEquals('group2', $group1->getName()); + } + + public function testShouldUseRemoverWhenRemoveAllItems() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new CollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_type' => 'collection_entry' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->once()) + ->method('removeGroup') + ->with($this->identicalTo($group1)); + + $form->submit(['groups' => []]); + $this->assertTrue($form->isSynchronized()); + } + + public function testShouldValidateEntryEntity() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new CollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_type' => 'collection_entry' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->never()) + ->method('removeGroup'); + + $form->submit(['groups' => ['']]); + $this->assertTrue($form->isSynchronized()); + $this->assertFalse($form->isValid()); + $this->assertCount(0, $form->getErrors()); + $this->assertCount(0, $form->get('groups')->getErrors()); + $this->assertCount(1, $form->get('groups')->get(0)->getErrors()); + } + + public function testWithInvalidValue() + { + $form = $this->factory->create( + new CollectionType(), + null, + [ + 'entry_data_class' => Group::class, + 'entry_type' => 'collection_entry' + ] + ); + $form->submit('test'); + $this->assertFalse($form->isSynchronized()); + } + + public function testGetName() + { + $type = new CollectionType(); + $this->assertEquals('oro_api_collection', $type->getName()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/CompoundEntityTypeTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/CompoundEntityTypeTest.php new file mode 100644 index 00000000000..de30aa6bf61 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/CompoundEntityTypeTest.php @@ -0,0 +1,194 @@ + new NameContainerType()], + [] + ) + ]; + } + + public function testBuildFormForField() + { + $metadata = new EntityMetadata(); + $metadata->addField(new FieldMetadata('name')); + + $config = new EntityDefinitionConfig(); + $config->addField('name'); + + $data = new Entity\User(); + $form = $this->factory->create( + new CompoundEntityType(), + $data, + [ + 'data_class' => Entity\User::class, + 'metadata' => $metadata, + 'config' => $config, + ] + ); + $form->submit(['name' => 'testName']); + $this->assertTrue($form->isSynchronized()); + $this->assertEquals('testName', $data->getName()); + } + + public function testBuildFormForRenamedField() + { + $metadata = new EntityMetadata(); + $metadata->addField(new FieldMetadata('renamedName')); + + $config = new EntityDefinitionConfig(); + $config->addField('renamedName')->setPropertyPath('name'); + + $data = new Entity\User(); + $form = $this->factory->create( + new CompoundEntityType(), + $data, + [ + 'data_class' => Entity\User::class, + 'metadata' => $metadata, + 'config' => $config, + ] + ); + $form->submit(['renamedName' => 'testName']); + $this->assertTrue($form->isSynchronized()); + $this->assertEquals('testName', $data->getName()); + } + + public function testBuildFormForFieldWithFormType() + { + $metadata = new EntityMetadata(); + $metadata->addField(new FieldMetadata('id')); + + $config = new EntityDefinitionConfig(); + $config->addField('id')->setFormType('integer'); + + $data = new Entity\User(); + $form = $this->factory->create( + new CompoundEntityType(), + $data, + [ + 'data_class' => Entity\User::class, + 'metadata' => $metadata, + 'config' => $config, + ] + ); + $form->submit(['id' => '123']); + $this->assertTrue($form->isSynchronized()); + $this->assertSame(123, $data->getId()); + } + + public function testBuildFormForFieldWithFormOptions() + { + $metadata = new EntityMetadata(); + $metadata->addField(new FieldMetadata('renamedName')); + + $config = new EntityDefinitionConfig(); + $config->addField('renamedName')->setFormOptions(['property_path' => 'name']); + + $data = new Entity\User(); + $form = $this->factory->create( + new CompoundEntityType(), + $data, + [ + 'data_class' => Entity\User::class, + 'metadata' => $metadata, + 'config' => $config, + ] + ); + $form->submit(['renamedName' => 'testName']); + $this->assertTrue($form->isSynchronized()); + $this->assertEquals('testName', $data->getName()); + } + + public function testBuildFormForIgnoredField() + { + $metadata = new EntityMetadata(); + $metadata->addField(new FieldMetadata('name')); + + $config = new EntityDefinitionConfig(); + $config->addField('name')->setPropertyPath('_'); + + $data = new Entity\User(); + $form = $this->factory->create( + new CompoundEntityType(), + $data, + [ + 'data_class' => Entity\User::class, + 'metadata' => $metadata, + 'config' => $config, + ] + ); + $form->submit(['name' => 'testName']); + $this->assertTrue($form->isSynchronized()); + $this->assertNull($data->getName()); + } + + public function testBuildFormForAssociation() + { + $metadata = new EntityMetadata(); + $metadata->addAssociation(new AssociationMetadata('owner')); + + $config = new EntityDefinitionConfig(); + $config->addField('owner'); + + $data = new Entity\User(); + $form = $this->factory->create( + new CompoundEntityType(), + $data, + [ + 'data_class' => Entity\User::class, + 'metadata' => $metadata, + 'config' => $config, + ] + ); + $form->submit(['owner' => ['name' => 'testName']]); + $this->assertTrue($form->isSynchronized()); + $this->assertNull($data->getOwner()); + } + + public function testBuildFormForAssociationAsField() + { + $metadata = new EntityMetadata(); + $metadata->addAssociation(new AssociationMetadata('owner'))->setDataType('scalar'); + + $config = new EntityDefinitionConfig(); + $field = $config->addField('owner'); + $field->setFormType('test_name_container'); + $field->setFormOptions(['data_class' => Entity\User::class]); + + $data = new Entity\User(); + $form = $this->factory->create( + new CompoundEntityType(), + $data, + [ + 'data_class' => Entity\User::class, + 'metadata' => $metadata, + 'config' => $config, + ] + ); + $form->submit(['owner' => ['name' => 'testName']]); + $this->assertTrue($form->isSynchronized()); + $this->assertNotNull($data->getOwner()); + $this->assertSame('testName', $data->getOwner()->getName()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityCollectionTypeTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityCollectionTypeTest.php new file mode 100644 index 00000000000..bfdb9ff3460 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityCollectionTypeTest.php @@ -0,0 +1,86 @@ + new CollectionType(), + 'collection_entry' => new CollectionEntryType() + ], + [] + ) + ]; + } + + public function testShouldClearCollectionWhenRemoveAllItems() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|ArrayCollection $groups */ + $groups = $this->getMockBuilder(ArrayCollection::class) + ->setMethods(['clear']) + ->getMock(); + + $groups->expects($this->once()) + ->method('clear'); + + $entity = new User(); + $entity->setGroups($groups); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new EntityCollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_type' => 'collection_entry' + ] + ); + $form = $formBuilder->getForm(); + + $form->submit(['groups' => []]); + $this->assertTrue($form->isSynchronized()); + } + + public function testGetName() + { + $type = new EntityCollectionType(); + $this->assertEquals('oro_api_entity_collection', $type->getName()); + } + + public function testGetParent() + { + $type = new EntityCollectionType(); + $this->assertEquals('oro_api_collection', $type->getParent()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityScalarCollectionTypeTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityScalarCollectionTypeTest.php new file mode 100644 index 00000000000..04d1735fed9 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityScalarCollectionTypeTest.php @@ -0,0 +1,83 @@ + new ScalarCollectionType()], + [] + ) + ]; + } + + public function testShouldClearCollectionWhenRemoveAllItems() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|ArrayCollection $groups */ + $groups = $this->getMockBuilder(ArrayCollection::class) + ->setMethods(['clear']) + ->getMock(); + + $groups->expects($this->once()) + ->method('clear'); + + $entity = new User(); + $entity->setGroups($groups); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new EntityScalarCollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_data_property' => 'name' + ] + ); + $form = $formBuilder->getForm(); + + $form->submit(['groups' => []]); + $this->assertTrue($form->isSynchronized()); + } + + public function testGetName() + { + $type = new EntityScalarCollectionType(); + $this->assertEquals('oro_api_entity_scalar_collection', $type->getName()); + } + + public function testGetParent() + { + $type = new EntityScalarCollectionType(); + $this->assertEquals('oro_api_scalar_collection', $type->getParent()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityTypeTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityTypeTest.php index 41a682d00d8..0ec4f50058f 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityTypeTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Form/Type/EntityTypeTest.php @@ -1,6 +1,6 @@ getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + $group2 = new Group(); + $group2->setName('group2'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new ScalarCollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_data_property' => 'name' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->once()) + ->method('addGroup') + ->with($group2); + $entity->expects($this->never()) + ->method('removeGroup'); + + $form->submit(['groups' => ['group1', 'group2']]); + $this->assertTrue($form->isSynchronized()); + } + + public function testShouldUseRemover() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + $group2 = new Group(); + $group2->setId(2); + $group2->setName('group2'); + + $entity->getGroups()->add($group1); + $entity->getGroups()->add($group2); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new ScalarCollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_data_property' => 'name' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->once()) + ->method('removeGroup') + ->with($this->identicalTo($group2)); + + $form->submit(['groups' => ['group1']]); + $this->assertTrue($form->isSynchronized()); + } + + public function testShouldUpdateExistingEntity() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new ScalarCollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_data_property' => 'name' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->never()) + ->method('removeGroup'); + + $form->submit(['groups' => ['group2']]); + $this->assertTrue($form->isSynchronized()); + + $this->assertEquals('group2', $group1->getName()); + } + + public function testShouldUseRemoverWhenRemoveAllItems() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new ScalarCollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_data_property' => 'name' + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->once()) + ->method('removeGroup') + ->with($this->identicalTo($group1)); + + $form->submit(['groups' => []]); + $this->assertTrue($form->isSynchronized()); + } + + public function testShouldValidateEntryEntity() + { + /** @var \PHPUnit_Framework_MockObject_MockObject|User $entity */ + $entity = $this->getMockBuilder(User::class) + ->setMethods(['addGroup', 'removeGroup']) + ->getMock(); + + $group1 = new Group(); + $group1->setId(1); + $group1->setName('group1'); + + $entity->getGroups()->add($group1); + + $formBuilder = $this->factory->createBuilder( + FormType::class, + $entity, + ['data_class' => User::class] + ); + $formBuilder->add( + 'groups', + new ScalarCollectionType(), + [ + 'entry_data_class' => Group::class, + 'entry_data_property' => 'name', + 'entry_options' => [ + 'constraints' => [new Assert\NotBlank()] + ] + ] + ); + $form = $formBuilder->getForm(); + + $entity->expects($this->never()) + ->method('addGroup'); + $entity->expects($this->never()) + ->method('removeGroup'); + + $form->submit(['groups' => ['']]); + $this->assertTrue($form->isSynchronized()); + $this->assertFalse($form->isValid()); + $this->assertCount(0, $form->getErrors()); + $this->assertCount(0, $form->get('groups')->getErrors()); + $this->assertCount(1, $form->get('groups')->get(0)->getErrors()); + } + + public function testWithInvalidValue() + { + $form = $this->factory->create( + new ScalarCollectionType(), + null, + [ + 'entry_data_class' => Group::class, + 'entry_data_property' => 'name' + ] + ); + $form->submit('test'); + $this->assertFalse($form->isSynchronized()); + } + + public function testGetName() + { + $type = new ScalarCollectionType(); + $this->assertEquals('oro_api_scalar_collection', $type->getName()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/AssociationMetadataTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/AssociationMetadataTest.php index e2dd7b1a304..e2a1acc9bdb 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/AssociationMetadataTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/AssociationMetadataTest.php @@ -26,6 +26,7 @@ public function testClone() $associationMetadata->setAcceptableTargetClassNames(['targetClassName1']); $associationMetadata->setIsCollection(true); $associationMetadata->setIsNullable(true); + $associationMetadata->setCollapsed(true); $targetEntityMetadata = new EntityMetadata(); $targetEntityMetadata->setClassName('TargetEntityClassName'); $associationMetadata->setTargetMetadata($targetEntityMetadata); @@ -57,17 +58,19 @@ public function testToArray() $associationMetadata->setAssociationType('manyToMany'); $associationMetadata->setIsCollection(true); $associationMetadata->setIsNullable(true); + $associationMetadata->setCollapsed(true); $associationMetadata->setTargetMetadata($this->entityMetadata); $this->assertEquals( [ 'name' => 'testName', 'data_type' => 'testDataType', - 'target_class' => 'targetClassName', - 'acceptable_target_classes' => ['targetClassName1', 'targetClassName2'], + 'nullable' => true, + 'collapsed' => true, 'association_type' => 'manyToMany', 'collection' => true, - 'nullable' => true, + 'target_class' => 'targetClassName', + 'acceptable_target_classes' => ['targetClassName1', 'targetClassName2'], 'target_metadata' => [ 'class' => 'entityClassName', 'inherited' => true @@ -84,7 +87,12 @@ public function testToArrayWithRequiredPropertiesOnly() $this->assertEquals( [ - 'name' => 'testName' + 'name' => 'testName', + 'data_type' => null, + 'nullable' => false, + 'collapsed' => false, + 'association_type' => null, + 'collection' => false, ], $associationMetadata->toArray() ); @@ -168,6 +176,15 @@ public function testNullable() $this->assertTrue($associationMetadata->isNullable()); } + public function testCollapsed() + { + $associationMetadata = new AssociationMetadata(); + + $this->assertFalse($associationMetadata->isCollapsed()); + $associationMetadata->setCollapsed(true); + $this->assertTrue($associationMetadata->isCollapsed()); + } + public function testTargetMetadata() { $associationMetadata = new AssociationMetadata(); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/EntityMetadataTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/EntityMetadataTest.php index 72f71c31188..7801f64e213 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/EntityMetadataTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Metadata/EntityMetadataTest.php @@ -86,7 +86,11 @@ public function testToArray() ], 'associations' => [ 'association1' => [ - 'data_type' => 'testDataType' + 'data_type' => 'testDataType', + 'nullable' => false, + 'collapsed' => false, + 'association_type' => null, + 'collection' => false, ] ], ], diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/ByConfigObjectNormalizerTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/ByConfigObjectNormalizerTest.php index db0ddf99101..a773875f29a 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/ByConfigObjectNormalizerTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/ByConfigObjectNormalizerTest.php @@ -9,6 +9,7 @@ use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Config\FiltersConfigExtension; use Oro\Bundle\ApiBundle\Config\SortersConfigExtension; +use Oro\Bundle\ApiBundle\Normalizer\ConfigNormalizer; use Oro\Bundle\ApiBundle\Normalizer\DateTimeNormalizer; use Oro\Bundle\ApiBundle\Normalizer\ObjectNormalizer; use Oro\Bundle\ApiBundle\Normalizer\ObjectNormalizerRegistry; @@ -35,7 +36,8 @@ protected function setUp() $normalizers, new DoctrineHelper($doctrine), new EntityDataAccessor(), - new EntityDataTransformer($this->getMock('Symfony\Component\DependencyInjection\ContainerInterface')) + new EntityDataTransformer($this->getMock('Symfony\Component\DependencyInjection\ContainerInterface')), + new ConfigNormalizer() ); $normalizers->addNormalizer( @@ -738,6 +740,118 @@ public function testNormalizeObjectWithTableInheritanceRelations() ); } + public function testNormalizeShouldNotChangeOriginalConfig() + { + $object = new Object\Group(); + $object->setId(123); + $object->setName('test_name'); + + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'name' => [ + 'depends_on' => ['id'] + ] + ] + ]; + + $configObject = $this->createConfigObject($config); + $srcConfig = $configObject->toArray(); + $this->objectNormalizer->normalizeObject($object, $configObject); + + $this->assertEquals($srcConfig, $configObject->toArray()); + } + + public function testNormalizeWithIgnoredField() + { + $object = new Object\Group(); + $object->setId(123); + $object->setName('test_name'); + + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'id' => null, + 'name1' => [ + 'property_path' => '_' + ] + ] + ]; + + $result = $this->objectNormalizer->normalizeObject( + $object, + $this->createConfigObject($config) + ); + + $this->assertEquals( + [ + 'id' => 123, + ], + $result + ); + } + + public function testNormalizeWithDependsOnNotConfiguredField() + { + $object = new Object\Group(); + $object->setId(123); + $object->setName('test_name'); + + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'name' => [ + 'depends_on' => ['id'] + ] + ] + ]; + + $result = $this->objectNormalizer->normalizeObject( + $object, + $this->createConfigObject($config) + ); + + $this->assertEquals( + [ + 'id' => 123, + 'name' => 'test_name', + ], + $result + ); + } + + public function testNormalizeWithDependsOnExcludedField() + { + $object = new Object\Group(); + $object->setId(123); + $object->setName('test_name'); + + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'id' => [ + 'exclude' => true + ], + 'name' => [ + 'depends_on' => ['id'] + ] + ] + ]; + + $result = $this->objectNormalizer->normalizeObject( + $object, + $this->createConfigObject($config) + ); + + $this->assertEquals( + [ + 'id' => 123, + 'name' => 'test_name', + ], + $result + ); + } + /** * @return Object\Product */ diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/ConfigNormalizerTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/ConfigNormalizerTest.php new file mode 100644 index 00000000000..95d5b7ab132 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/ConfigNormalizerTest.php @@ -0,0 +1,387 @@ +getLoader(ConfigUtil::DEFINITION); + + /** @var EntityDefinitionConfig $normalizedConfig */ + $normalizedConfig = $configLoader->load($config); + $normalizer->normalizeConfig($normalizedConfig); + + $this->assertEquals($expectedConfig, $normalizedConfig->toArray()); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function normalizeConfigProvider() + { + return [ + 'ignored fields' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'property_path' => '_' + ], + 'field2' => [ + 'property_path' => 'realField2' + ], + 'association1' => [ + 'fields' => [ + 'association11' => [ + 'fields' => [ + 'field111' => [ + 'property_path' => '_' + ], + 'field112' => null + ] + ], + ] + ], + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'property_path' => 'realField2' + ], + 'association1' => [ + 'fields' => [ + 'association11' => [ + 'fields' => [ + 'field112' => null + ] + ], + ] + ], + ] + ] + ], + 'field depends on another field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => null, + 'field2' => [ + 'depends_on' => ['field1'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => null, + 'field2' => [ + 'depends_on' => ['field1'] + ] + ] + ] + ], + 'field depends on excluded field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'exclude' => true + ], + 'field2' => [ + 'depends_on' => ['field1'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => null, + 'field2' => [ + 'depends_on' => ['field1'] + ] + ] + ] + ], + 'excluded field depends on another excluded field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'exclude' => true + ], + 'field2' => [ + 'exclude' => true, + 'depends_on' => ['field1'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'exclude' => true, + ], + 'field2' => [ + 'exclude' => true, + 'depends_on' => ['field1'] + ] + ] + ] + ], + 'field depends on excluded computed field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'exclude' => true + ], + 'field2' => [ + 'exclude' => true, + 'depends_on' => ['field1'] + ], + 'field3' => [ + 'depends_on' => ['field2'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => null, + 'field2' => [ + 'depends_on' => ['field1'] + ], + 'field3' => [ + 'depends_on' => ['field2'] + ] + ] + ] + ], + 'nested field depends on another field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field' => [ + 'fields' => [ + 'field1' => [ + 'exclude' => true, + ], + 'field2' => [ + 'depends_on' => ['field1'] + ] + ] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field' => [ + 'fields' => [ + 'field1' => null, + 'field2' => [ + 'depends_on' => ['field1'] + ] + ] + ] + ] + ] + ], + 'field depends on association child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on association undefined child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field12' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null, + 'field12' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on undefined association child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on association excluded child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => [ + 'exclude' => true + ] + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on excluded association child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'exclude' => true, + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on excluded association and its child fields' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'depends_on' => ['association1.association11.field111'] + ], + 'association1' => [ + 'exclude' => true, + 'fields' => [ + 'association11' => [ + 'exclude' => true, + 'fields' => [ + 'field111' => [ + 'exclude' => true + ] + ] + ], + ] + ], + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'depends_on' => ['association1.association11.field111'] + ], + 'association1' => [ + 'fields' => [ + 'association11' => [ + 'fields' => [ + 'field111' => null + ] + ], + ] + ], + ] + ] + ], + ]; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/EntityObjectNormalizerTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/EntityObjectNormalizerTest.php index 186e71ad859..addc5c82950 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/EntityObjectNormalizerTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/EntityObjectNormalizerTest.php @@ -4,6 +4,7 @@ use Oro\Component\EntitySerializer\EntityDataAccessor; use Oro\Component\EntitySerializer\EntityDataTransformer; +use Oro\Bundle\ApiBundle\Normalizer\ConfigNormalizer; use Oro\Bundle\ApiBundle\Normalizer\DateTimeNormalizer; use Oro\Bundle\ApiBundle\Normalizer\ObjectNormalizer; use Oro\Bundle\ApiBundle\Normalizer\ObjectNormalizerRegistry; @@ -26,7 +27,8 @@ protected function setUp() $normalizers, $this->doctrineHelper, new EntityDataAccessor(), - new EntityDataTransformer($this->getMock('Symfony\Component\DependencyInjection\ContainerInterface')) + new EntityDataTransformer($this->getMock('Symfony\Component\DependencyInjection\ContainerInterface')), + new ConfigNormalizer() ); $normalizers->addNormalizer( diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/PlainObjectNormalizerTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/PlainObjectNormalizerTest.php index 2364fd9ce7f..77ae34d03b4 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/PlainObjectNormalizerTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Normalizer/PlainObjectNormalizerTest.php @@ -4,6 +4,7 @@ use Oro\Component\EntitySerializer\EntityDataAccessor; use Oro\Component\EntitySerializer\EntityDataTransformer; +use Oro\Bundle\ApiBundle\Normalizer\ConfigNormalizer; use Oro\Bundle\ApiBundle\Normalizer\DateTimeNormalizer; use Oro\Bundle\ApiBundle\Normalizer\ObjectNormalizer; use Oro\Bundle\ApiBundle\Normalizer\ObjectNormalizerRegistry; @@ -29,7 +30,8 @@ protected function setUp() $normalizers, new DoctrineHelper($doctrine), new EntityDataAccessor(), - new EntityDataTransformer($this->getMock('Symfony\Component\DependencyInjection\ContainerInterface')) + new EntityDataTransformer($this->getMock('Symfony\Component\DependencyInjection\ContainerInterface')), + new ConfigNormalizer() ); $normalizers->addNormalizer( diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectResources/CollectResourcesContextTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectResources/CollectResourcesContextTest.php new file mode 100644 index 00000000000..3b012b282a9 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectResources/CollectResourcesContextTest.php @@ -0,0 +1,32 @@ +context = new CollectResourcesContext(); + } + + public function testResultShouldBeInitialized() + { + $this->assertInstanceOf( + 'Oro\Bundle\ApiBundle\Request\ApiResourceCollection', + $this->context->getResult() + ); + } + + public function testAccessibleResources() + { + $this->assertEquals([], $this->context->getAccessibleResources()); + + $this->context->setAccessibleResources(['Test\Class']); + $this->assertEquals(['Test\Class'], $this->context->getAccessibleResources()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectResources/LoadAccessibleResourcesTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectResources/LoadAccessibleResourcesTest.php new file mode 100644 index 00000000000..a5c97eac6ba --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectResources/LoadAccessibleResourcesTest.php @@ -0,0 +1,47 @@ +processor = new LoadAccessibleResources(); + } + + public function testProcessWhenAccessibleResourcesAreAlreadyBuilt() + { + $context = new CollectResourcesContext(); + $context->setAccessibleResources(['Test\Entity1']); + + $this->processor->process($context); + + $this->assertEquals(['Test\Entity1'], $context->getAccessibleResources()); + } + + public function testProcess() + { + $context = new CollectResourcesContext(); + + $resources = $context->getResult(); + $resources->add(new ApiResource('Test\Entity1')); + $resources->add(new ApiResource('Test\Entity2')); + $resources->get('Test\Entity2')->setExcludedActions(['get']); + $resources->add(new ApiResource('Test\Entity3')); + $resources->get('Test\Entity3')->setExcludedActions(['get_list']); + + $this->processor->process($context); + + $this->assertEquals( + ['Test\Entity1', 'Test\Entity3'], + $context->getAccessibleResources() + ); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/CollectSubresourcesContextTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/CollectSubresourcesContextTest.php index 2997492ba7c..621d99c25dd 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/CollectSubresourcesContextTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/CollectSubresourcesContextTest.php @@ -7,25 +7,12 @@ class CollectSubresourcesContextTest extends \PHPUnit_Framework_TestCase { - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $configProvider; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $metadataProvider; - /** @var CollectSubresourcesContext */ protected $context; protected function setUp() { - $this->configProvider = $this->getMockBuilder('Oro\Bundle\ApiBundle\Provider\ConfigProvider') - ->disableOriginalConstructor() - ->getMock(); - $this->metadataProvider = $this->getMockBuilder('Oro\Bundle\ApiBundle\Provider\MetadataProvider') - ->disableOriginalConstructor() - ->getMock(); - - $this->context = new CollectSubresourcesContext($this->configProvider, $this->metadataProvider); + $this->context = new CollectSubresourcesContext(); } public function testResultShouldBeInitialized() @@ -48,4 +35,12 @@ public function testResources() $this->assertTrue($this->context->hasResource('Test\Class')); $this->assertSame($resource, $this->context->getResource('Test\Class')); } + + public function testAccessibleResources() + { + $this->assertEquals([], $this->context->getAccessibleResources()); + + $this->context->setAccessibleResources(['Test\Class']); + $this->assertEquals(['Test\Class'], $this->context->getAccessibleResources()); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/InitializeSubresourcesTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/InitializeSubresourcesTest.php index c7ad178cc19..0dd13ec5d86 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/InitializeSubresourcesTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CollectSubresources/InitializeSubresourcesTest.php @@ -52,6 +52,187 @@ public function testProcessWhenSubresourcesAreAlreadyInitialized() ); } + public function testProcessForExcludedAssociation() + { + $resource = new ApiResource('Test\Class'); + + $resourceConfig = new Config(); + $resourceConfig->setDefinition(new EntityDefinitionConfig()); + $resourceConfig->getDefinition()->addField('association1')->setExcluded(); + $resourceMetadata = new EntityMetadata(); + $association = new AssociationMetadata(); + $association->setName('association1'); + $association->setTargetClassName('Test\Association1Target'); + $association->setAcceptableTargetClassNames(['Test\Association1Target']); + $association->setIsCollection(false); + $resourceMetadata->addAssociation($association); + + $this->context->getRequestType()->add(RequestType::REST); + $this->context->setVersion('1.1'); + $this->context->setResources([$resource]); + $this->context->setAccessibleResources([]); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->with( + $resource->getEntityClass(), + $this->context->getVersion(), + $this->context->getRequestType(), + [new EntityDefinitionConfigExtra()] + ) + ->willReturn($resourceConfig); + $this->metadataProvider->expects($this->once()) + ->method('getMetadata') + ->with( + $resource->getEntityClass(), + $this->context->getVersion(), + $this->context->getRequestType(), + $resourceConfig->getDefinition(), + [], + true + ) + ->willReturn($resourceMetadata); + + $this->processor->process($this->context); + + $expectedSubresources = new ApiResourceSubresources($resource->getEntityClass()); + + $this->assertEquals( + ['Test\Class' => $expectedSubresources], + $this->context->getResult()->toArray() + ); + } + + public function testProcessForToOneAssociationForNotAccessibleResource() + { + $resource = new ApiResource('Test\Class'); + + $resourceConfig = new Config(); + $resourceConfig->setDefinition(new EntityDefinitionConfig()); + $resourceMetadata = new EntityMetadata(); + $toOneAssociation = new AssociationMetadata(); + $toOneAssociation->setName('association1'); + $toOneAssociation->setTargetClassName('Test\Association1Target'); + $toOneAssociation->setAcceptableTargetClassNames(['Test\Association1Target']); + $toOneAssociation->setIsCollection(false); + $resourceMetadata->addAssociation($toOneAssociation); + + $this->context->getRequestType()->add(RequestType::REST); + $this->context->setVersion('1.1'); + $this->context->setResources([$resource]); + $this->context->setAccessibleResources([]); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->with( + $resource->getEntityClass(), + $this->context->getVersion(), + $this->context->getRequestType(), + [new EntityDefinitionConfigExtra()] + ) + ->willReturn($resourceConfig); + $this->metadataProvider->expects($this->once()) + ->method('getMetadata') + ->with( + $resource->getEntityClass(), + $this->context->getVersion(), + $this->context->getRequestType(), + $resourceConfig->getDefinition(), + [], + true + ) + ->willReturn($resourceMetadata); + + $this->processor->process($this->context); + + $expectedSubresources = new ApiResourceSubresources($resource->getEntityClass()); + $toOneAssociationSubresource = new ApiSubresource(); + $toOneAssociationSubresource->setTargetClassName($toOneAssociation->getTargetClassName()); + $toOneAssociationSubresource->setAcceptableTargetClassNames( + $toOneAssociation->getAcceptableTargetClassNames() + ); + $toOneAssociationSubresource->setIsCollection($toOneAssociation->isCollection()); + $toOneAssociationSubresource->setExcludedActions( + [ + 'get_subresource', + 'get_relationship', + 'update_relationship', + ] + ); + $expectedSubresources->addSubresource($toOneAssociation->getName(), $toOneAssociationSubresource); + + $this->assertEquals( + ['Test\Class' => $expectedSubresources], + $this->context->getResult()->toArray() + ); + } + + public function testProcessForToManyAssociationForNotAccessibleResource() + { + $resource = new ApiResource('Test\Class'); + + $resourceConfig = new Config(); + $resourceConfig->setDefinition(new EntityDefinitionConfig()); + $resourceMetadata = new EntityMetadata(); + $toManyAssociation = new AssociationMetadata(); + $toManyAssociation->setName('association1'); + $toManyAssociation->setTargetClassName('Test\Association1Target'); + $toManyAssociation->setAcceptableTargetClassNames(['Test\Association1Target']); + $toManyAssociation->setIsCollection(true); + $resourceMetadata->addAssociation($toManyAssociation); + + $this->context->getRequestType()->add(RequestType::REST); + $this->context->setVersion('1.1'); + $this->context->setResources([$resource]); + $this->context->setAccessibleResources([]); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->with( + $resource->getEntityClass(), + $this->context->getVersion(), + $this->context->getRequestType(), + [new EntityDefinitionConfigExtra()] + ) + ->willReturn($resourceConfig); + $this->metadataProvider->expects($this->once()) + ->method('getMetadata') + ->with( + $resource->getEntityClass(), + $this->context->getVersion(), + $this->context->getRequestType(), + $resourceConfig->getDefinition(), + [], + true + ) + ->willReturn($resourceMetadata); + + $this->processor->process($this->context); + + $expectedSubresources = new ApiResourceSubresources($resource->getEntityClass()); + $toManyAssociationSubresource = new ApiSubresource(); + $toManyAssociationSubresource->setTargetClassName($toManyAssociation->getTargetClassName()); + $toManyAssociationSubresource->setAcceptableTargetClassNames( + $toManyAssociation->getAcceptableTargetClassNames() + ); + $toManyAssociationSubresource->setIsCollection($toManyAssociation->isCollection()); + $toManyAssociationSubresource->setExcludedActions( + [ + 'get_subresource', + 'get_relationship', + 'update_relationship', + 'add_relationship', + 'delete_relationship' + ] + ); + $expectedSubresources->addSubresource($toManyAssociation->getName(), $toManyAssociationSubresource); + + $this->assertEquals( + ['Test\Class' => $expectedSubresources], + $this->context->getResult()->toArray() + ); + } + public function testProcessForToOneAssociationForResourceWithoutExcludedActions() { $resource = new ApiResource('Test\Class'); @@ -60,15 +241,16 @@ public function testProcessForToOneAssociationForResourceWithoutExcludedActions( $resourceConfig->setDefinition(new EntityDefinitionConfig()); $resourceMetadata = new EntityMetadata(); $toOneAssociation = new AssociationMetadata(); - $toOneAssociation->setName('association2'); - $toOneAssociation->setTargetClassName('Test\Association2Target'); - $toOneAssociation->setAcceptableTargetClassNames(['Test\Association2Target']); + $toOneAssociation->setName('association1'); + $toOneAssociation->setTargetClassName('Test\Association1Target'); + $toOneAssociation->setAcceptableTargetClassNames(['Test\Association1Target']); $toOneAssociation->setIsCollection(false); $resourceMetadata->addAssociation($toOneAssociation); $this->context->getRequestType()->add(RequestType::REST); $this->context->setVersion('1.1'); $this->context->setResources([$resource]); + $this->context->setAccessibleResources(['Test\Association1Target']); $this->configProvider->expects($this->once()) ->method('getConfig') @@ -86,7 +268,8 @@ public function testProcessForToOneAssociationForResourceWithoutExcludedActions( $this->context->getVersion(), $this->context->getRequestType(), $resourceConfig->getDefinition(), - [] + [], + true ) ->willReturn($resourceMetadata); @@ -125,6 +308,7 @@ public function testProcessForToManyAssociationForResourceWithoutExcludedActions $this->context->getRequestType()->add(RequestType::REST); $this->context->setVersion('1.1'); $this->context->setResources([$resource]); + $this->context->setAccessibleResources(['Test\Association1Target']); $this->configProvider->expects($this->once()) ->method('getConfig') @@ -142,7 +326,8 @@ public function testProcessForToManyAssociationForResourceWithoutExcludedActions $this->context->getVersion(), $this->context->getRequestType(), $resourceConfig->getDefinition(), - [] + [], + true ) ->willReturn($resourceMetadata); @@ -172,15 +357,16 @@ public function testProcessForToOneAssociationForResourceWithExcludedActions() $resourceConfig->setDefinition(new EntityDefinitionConfig()); $resourceMetadata = new EntityMetadata(); $toOneAssociation = new AssociationMetadata(); - $toOneAssociation->setName('association2'); - $toOneAssociation->setTargetClassName('Test\Association2Target'); - $toOneAssociation->setAcceptableTargetClassNames(['Test\Association2Target']); + $toOneAssociation->setName('association1'); + $toOneAssociation->setTargetClassName('Test\Association1Target'); + $toOneAssociation->setAcceptableTargetClassNames(['Test\Association1Target']); $toOneAssociation->setIsCollection(false); $resourceMetadata->addAssociation($toOneAssociation); $this->context->getRequestType()->add(RequestType::REST); $this->context->setVersion('1.1'); $this->context->setResources([$resource]); + $this->context->setAccessibleResources(['Test\Association1Target']); $this->configProvider->expects($this->once()) ->method('getConfig') @@ -198,7 +384,8 @@ public function testProcessForToOneAssociationForResourceWithExcludedActions() $this->context->getVersion(), $this->context->getRequestType(), $resourceConfig->getDefinition(), - [] + [], + true ) ->willReturn($resourceMetadata); @@ -238,6 +425,7 @@ public function testProcessForToManyAssociationForResourceWithExcludedActions() $this->context->getRequestType()->add(RequestType::REST); $this->context->setVersion('1.1'); $this->context->setResources([$resource]); + $this->context->setAccessibleResources(['Test\Association1Target']); $this->configProvider->expects($this->once()) ->method('getConfig') @@ -255,7 +443,8 @@ public function testProcessForToManyAssociationForResourceWithExcludedActions() $this->context->getVersion(), $this->context->getRequestType(), $resourceConfig->getDefinition(), - [] + [], + true ) ->willReturn($resourceMetadata); @@ -285,15 +474,16 @@ public function testProcessForToOneAssociationForResourceWithUpdateInExcludedAct $resourceConfig->setDefinition(new EntityDefinitionConfig()); $resourceMetadata = new EntityMetadata(); $toOneAssociation = new AssociationMetadata(); - $toOneAssociation->setName('association2'); - $toOneAssociation->setTargetClassName('Test\Association2Target'); - $toOneAssociation->setAcceptableTargetClassNames(['Test\Association2Target']); + $toOneAssociation->setName('association1'); + $toOneAssociation->setTargetClassName('Test\Association1Target'); + $toOneAssociation->setAcceptableTargetClassNames(['Test\Association1Target']); $toOneAssociation->setIsCollection(false); $resourceMetadata->addAssociation($toOneAssociation); $this->context->getRequestType()->add(RequestType::REST); $this->context->setVersion('1.1'); $this->context->setResources([$resource]); + $this->context->setAccessibleResources(['Test\Association1Target']); $this->configProvider->expects($this->once()) ->method('getConfig') @@ -311,7 +501,8 @@ public function testProcessForToOneAssociationForResourceWithUpdateInExcludedAct $this->context->getVersion(), $this->context->getRequestType(), $resourceConfig->getDefinition(), - [] + [], + true ) ->willReturn($resourceMetadata); @@ -353,6 +544,7 @@ public function testProcessForToManyAssociationForResourceWithUpdateInExcludedAc $this->context->getRequestType()->add(RequestType::REST); $this->context->setVersion('1.1'); $this->context->setResources([$resource]); + $this->context->setAccessibleResources(['Test\Association1Target']); $this->configProvider->expects($this->once()) ->method('getConfig') @@ -370,7 +562,8 @@ public function testProcessForToManyAssociationForResourceWithUpdateInExcludedAc $this->context->getVersion(), $this->context->getRequestType(), $resourceConfig->getDefinition(), - [] + [], + true ) ->willReturn($resourceMetadata); @@ -426,7 +619,8 @@ public function testProcessForAssociationWithoutTargetMetadata() $this->context->getVersion(), $this->context->getRequestType(), $resourceConfig->getDefinition(), - [] + [], + true ) ->willReturn(null); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetDataCustomizationHandlerTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetDataCustomizationHandlerTest.php index a3dfe81a723..a2d34fd6ee7 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetDataCustomizationHandlerTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetDataCustomizationHandlerTest.php @@ -4,7 +4,7 @@ use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Processor\Config\GetConfig\SetDataCustomizationHandler; -use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedDataContext; +use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedData\CustomizeLoadedDataContext; use Oro\Bundle\ApiBundle\Tests\Unit\Processor\Config\ConfigProcessorTestCase; class SetDataCustomizationHandlerTest extends ConfigProcessorTestCase diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetMaxRelatedEntitiesTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetMaxRelatedEntitiesTest.php index 0dd1c8f6a41..8d454f14d68 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetMaxRelatedEntitiesTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/GetConfig/SetMaxRelatedEntitiesTest.php @@ -285,6 +285,119 @@ public function testProcessForNotManageableEntityWithParentToOneAndChildToMany() ); } + public function testProcessForNotManageableEntityWhenToMayAssociationShouldBeRepresentedAsField() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'data_type' => 'array', + 'target_class' => 'Test\Target', + 'target_type' => 'to-many', + 'exclusion_policy' => 'all', + 'fields' => [ + 'field11' => null + ] + ] + ] + ]; + $limit = 100; + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(false); + + $configObject = $this->createConfigObject($config); + $this->context->setMaxRelatedEntities($limit); + $this->context->setResult($configObject); + $this->processor->process($this->context); + + $this->assertConfig( + [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'data_type' => 'array', + 'target_class' => 'Test\Target', + 'target_type' => 'to-many', + 'exclusion_policy' => 'all', + 'fields' => [ + 'field11' => null + ] + ] + ] + ], + $configObject + ); + } + + public function testProcessForManageableEntityWhenToMayAssociationShouldBeRepresentedAsField() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'data_type' => 'array', + 'exclusion_policy' => 'all', + 'fields' => [ + 'field11' => null + ] + ] + ] + ]; + $limit = 100; + + $rootEntityMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $rootEntityMetadata->expects($this->any()) + ->method('hasAssociation') + ->willReturnMap([['field1', true]]); + $rootEntityMetadata->expects($this->once()) + ->method('getAssociationTargetClass') + ->with('field1') + ->willReturn('Test\Field1Target'); + $rootEntityMetadata->expects($this->once()) + ->method('isCollectionValuedAssociation') + ->with('field1') + ->willReturn(true); + + $field1TargetEntityMetadata = $this->getClassMetadataMock('Test\Field1Target'); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->exactly(2)) + ->method('getEntityMetadataForClass') + ->willReturnMap( + [ + [self::TEST_CLASS_NAME, true, $rootEntityMetadata], + ['Test\Field1Target', true, $field1TargetEntityMetadata], + ] + ); + + $configObject = $this->createConfigObject($config); + $this->context->setMaxRelatedEntities($limit); + $this->context->setResult($configObject); + $this->processor->process($this->context); + + $this->assertConfig( + [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'data_type' => 'array', + 'exclusion_policy' => 'all', + 'fields' => [ + 'field11' => null + ] + ] + ] + ], + $configObject + ); + } + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/CompleteDefinitionTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/CompleteDefinitionTest.php index e075ee90828..0de8d053411 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/CompleteDefinitionTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/CompleteDefinitionTest.php @@ -9,6 +9,7 @@ use Oro\Bundle\ApiBundle\Model\EntityIdentifier; use Oro\Bundle\ApiBundle\Processor\Config\Shared\CompleteDefinition; use Oro\Bundle\ApiBundle\Tests\Unit\Processor\Config\ConfigProcessorTestCase; +use Oro\Bundle\ApiBundle\Util\ConfigUtil; /** * @SuppressWarnings(PHPMD.ExcessiveClassLength) @@ -1324,6 +1325,50 @@ public function testProcessIdentifierFieldsOnlyForManageableEntity() ); } + public function testProcessIdentifierFieldsOnlyForManageableEntityWithIgnoredPropertyPath() + { + $config = [ + 'fields' => [ + 'id' => null, + 'field1' => [ + 'property_path' => ConfigUtil::IGNORE_PROPERTY_PATH + ], + 'field2' => [ + 'property_path' => ConfigUtil::IGNORE_PROPERTY_PATH + ], + ] + ]; + + $rootEntityMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $rootEntityMetadata->expects($this->any()) + ->method('getIdentifierFieldNames') + ->willReturn(['id']); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->once()) + ->method('getEntityMetadataForClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn($rootEntityMetadata); + + $this->context->setExtras([new FilterIdentifierFieldsConfigExtra()]); + $this->context->setResult($this->createConfigObject($config)); + $this->processor->process($this->context); + + $this->assertConfig( + [ + 'exclusion_policy' => 'all', + 'identifier_field_names' => ['id'], + 'fields' => [ + 'id' => null + ] + ], + $this->context->getResult() + ); + } + public function testProcessIdentifierFieldsOnlyWhenNoIdFieldInConfigForManageableEntity() { $config = [ diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/ExcludeNotAccessibleRelationsTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/ExcludeNotAccessibleRelationsTest.php index ea18768061a..fa2bdd6460d 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/ExcludeNotAccessibleRelationsTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Config/Shared/ExcludeNotAccessibleRelationsTest.php @@ -203,7 +203,7 @@ public function testProcessForManageableEntity() ); } - public function testProcessWhenTargetEntityDoesNotHaveApiResource() + public function testProcessWhenTargetEntityDoesNotHaveAccessibleApiResource() { $config = [ 'exclusion_policy' => 'all', @@ -238,11 +238,64 @@ public function testProcessWhenTargetEntityDoesNotHaveApiResource() ); $this->resourcesProvider->expects($this->once()) ->method('isResourceAccessible') + ->with('Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType()) + ->willReturn(false); + + $this->context->setResult($this->createConfigObject($config)); + $this->processor->process($this->context); + + $this->assertConfig( + [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'exclude' => true + ] + ] + ], + $this->context->getResult() + ); + } + + public function testProcessForArrayAssociation() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'data_type' => 'array' + ] + ] + ]; + + $rootEntityMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $rootEntityMetadata->expects($this->once()) + ->method('hasAssociation') + ->with('association1') + ->willReturn(true); + $rootEntityMetadata->expects($this->once()) + ->method('getAssociationMapping') + ->with('association1') + ->willReturn(['targetEntity' => 'Test\Association1Target']); + + $association1Metadata = $this->getClassMetadataMock('Test\Association1Target'); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->exactly(2)) + ->method('getEntityMetadataForClass') ->willReturnMap( [ - ['Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType(), false], + [self::TEST_CLASS_NAME, true, $rootEntityMetadata], + ['Test\Association1Target', true, $association1Metadata], ] ); + $this->resourcesProvider->expects($this->once()) + ->method('isResourceKnown') + ->with('Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType()) + ->willReturn(true); $this->context->setResult($this->createConfigObject($config)); $this->processor->process($this->context); @@ -252,7 +305,64 @@ public function testProcessWhenTargetEntityDoesNotHaveApiResource() 'exclusion_policy' => 'all', 'fields' => [ 'association1' => [ - 'exclude' => true + 'data_type' => 'array' + ] + ] + ], + $this->context->getResult() + ); + } + + public function testProcessForArrayAssociationAndTargetEntityDoesNotHaveApiResource() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'data_type' => 'array' + ] + ] + ]; + + $rootEntityMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $rootEntityMetadata->expects($this->once()) + ->method('hasAssociation') + ->with('association1') + ->willReturn(true); + $rootEntityMetadata->expects($this->once()) + ->method('getAssociationMapping') + ->with('association1') + ->willReturn(['targetEntity' => 'Test\Association1Target']); + + $association1Metadata = $this->getClassMetadataMock('Test\Association1Target'); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->exactly(2)) + ->method('getEntityMetadataForClass') + ->willReturnMap( + [ + [self::TEST_CLASS_NAME, true, $rootEntityMetadata], + ['Test\Association1Target', true, $association1Metadata], + ] + ); + $this->resourcesProvider->expects($this->once()) + ->method('isResourceKnown') + ->with('Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType()) + ->willReturn(false); + + $this->context->setResult($this->createConfigObject($config)); + $this->processor->process($this->context); + + $this->assertConfig( + [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'data_type' => 'array', + 'exclude' => true ] ] ], @@ -297,11 +407,64 @@ public function testProcessWhenTargetEntityUsesTableInheritance() ); $this->resourcesProvider->expects($this->once()) ->method('isResourceAccessible') + ->with('Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType()) + ->willReturn(true); + + $this->context->setResult($this->createConfigObject($config)); + $this->processor->process($this->context); + + $this->assertConfig( + [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => null + ] + ], + $this->context->getResult() + ); + } + + public function testProcessForArrayAssociationWhenTargetEntityUsesTableInheritance() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'data_type' => 'array' + ] + ] + ]; + + $rootEntityMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $rootEntityMetadata->expects($this->once()) + ->method('hasAssociation') + ->with('association1') + ->willReturn(true); + $rootEntityMetadata->expects($this->once()) + ->method('getAssociationMapping') + ->with('association1') + ->willReturn(['targetEntity' => 'Test\Association1Target']); + + $association1Metadata = $this->getClassMetadataMock('Test\Association1Target'); + $association1Metadata->inheritanceType = ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE; + $association1Metadata->subClasses = ['Test\Association1Target1']; + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->exactly(2)) + ->method('getEntityMetadataForClass') ->willReturnMap( [ - ['Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType(), true], + [self::TEST_CLASS_NAME, true, $rootEntityMetadata], + ['Test\Association1Target', true, $association1Metadata], ] ); + $this->resourcesProvider->expects($this->once()) + ->method('isResourceKnown') + ->with('Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType()) + ->willReturn(true); $this->context->setResult($this->createConfigObject($config)); $this->processor->process($this->context); @@ -310,14 +473,16 @@ public function testProcessWhenTargetEntityUsesTableInheritance() [ 'exclusion_policy' => 'all', 'fields' => [ - 'association1' => null + 'association1' => [ + 'data_type' => 'array' + ] ] ], $this->context->getResult() ); } - public function testProcessWhenTargetEntityUsesTableInheritanceAndNoApiResourceForAnyConcreteTargetEntity() + public function testProcessWhenTargetEntityUsesTableInheritanceAndNoAccessibleApiResourceForAnyConcreteTarget() { $config = [ 'exclusion_policy' => 'all', @@ -376,4 +541,67 @@ public function testProcessWhenTargetEntityUsesTableInheritanceAndNoApiResourceF $this->context->getResult() ); } + + public function testProcessForArrayAssociationAndTargetUsesTableInheritanceAndNoApiResourceForAnyConcreteTarget() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'data_type' => 'array' + ] + ] + ]; + + $rootEntityMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $rootEntityMetadata->expects($this->once()) + ->method('hasAssociation') + ->with('association1') + ->willReturn(true); + $rootEntityMetadata->expects($this->once()) + ->method('getAssociationMapping') + ->with('association1') + ->willReturn(['targetEntity' => 'Test\Association1Target']); + + $association1Metadata = $this->getClassMetadataMock('Test\Association1Target'); + $association1Metadata->inheritanceType = ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE; + $association1Metadata->subClasses = ['Test\Association1Target1']; + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->exactly(2)) + ->method('getEntityMetadataForClass') + ->willReturnMap( + [ + [self::TEST_CLASS_NAME, true, $rootEntityMetadata], + ['Test\Association1Target', true, $association1Metadata], + ] + ); + $this->resourcesProvider->expects($this->exactly(2)) + ->method('isResourceKnown') + ->willReturnMap( + [ + ['Test\Association1Target', $this->context->getVersion(), $this->context->getRequestType(), false], + ['Test\Association1Target1', $this->context->getVersion(), $this->context->getRequestType(), false], + ] + ); + + $this->context->setResult($this->createConfigObject($config)); + $this->processor->process($this->context); + + $this->assertConfig( + [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'data_type' => 'array', + 'exclude' => true + ] + ] + ], + $this->context->getResult() + ); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/ContextConfigAccessorTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/ContextConfigAccessorTest.php new file mode 100644 index 00000000000..fec0bd5595b --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/ContextConfigAccessorTest.php @@ -0,0 +1,50 @@ +context = $this->getMockBuilder('Oro\Bundle\ApiBundle\Processor\Context') + ->disableOriginalConstructor() + ->getMock(); + + $this->configAccessor = new ContextConfigAccessor($this->context); + } + + public function testGetConfigForContextClass() + { + $className = 'Test\Entity'; + $config = new EntityDefinitionConfig(); + + $this->context->expects($this->once()) + ->method('getClassName') + ->willReturn($className); + $this->context->expects($this->once()) + ->method('getConfig') + ->willReturn($config); + + $this->assertSame($config, $this->configAccessor->getConfig($className)); + } + + public function testGetConfigForNotContextClass() + { + $this->context->expects($this->once()) + ->method('getClassName') + ->willReturn('Test\Entity1'); + $this->context->expects($this->never()) + ->method('getConfig'); + + $this->assertNull($this->configAccessor->getConfig('Test\Entity2')); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Create/SaveEntityTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Create/SaveEntityTest.php index cc8cf2001fe..9a299aa972d 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Create/SaveEntityTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Create/SaveEntityTest.php @@ -85,7 +85,7 @@ public function testProcessForManageableEntityWithSingleId() ->with($this->identicalTo($entity)); $em->expects($this->once()) ->method('flush') - ->with($this->identicalTo($entity)); + ->with(null); $this->context->setResult($entity); $this->processor->process($this->context); @@ -124,7 +124,7 @@ public function testProcessForManageableEntityWithCompositeId() ->with($this->identicalTo($entity)); $em->expects($this->once()) ->method('flush') - ->with($this->identicalTo($entity)); + ->with(null); $this->context->setResult($entity); $this->processor->process($this->context); @@ -162,7 +162,7 @@ public function testProcessForManageableEntityWhenIdWasNotGenerated() ->with($this->identicalTo($entity)); $em->expects($this->once()) ->method('flush') - ->with($this->identicalTo($entity)); + ->with(null); $this->context->setResult($entity); $this->processor->process($this->context); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/AbstractProcessorTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/AbstractProcessorTest.php new file mode 100644 index 00000000000..d2ca82e9be8 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/AbstractProcessorTest.php @@ -0,0 +1,59 @@ +setEvent($eventName); + + $processor = $this->getMockBuilder(AbstractProcessor::class) + ->setMethods(['processPreSubmit', 'processSubmit', 'processPostSubmit', 'processFinishSubmit']) + ->getMockForAbstractClass(); + + $processor->expects($this->once()) + ->method($expectedMethodName) + ->with($this->identicalTo($context)); + + $processor->process($context); + } + + public function processProvider() + { + return [ + ['pre_submit', 'processPreSubmit'], + ['submit', 'processSubmit'], + ['post_submit', 'processPostSubmit'], + ['finish_submit', 'processFinishSubmit'], + ]; + } + + public function testShouldNotCallAnyProcessMethodIfEventIsNotKnown() + { + $context = new CustomizeFormDataContext(); + $context->setEvent('unknown'); + + $processor = $this->getMockBuilder(AbstractProcessor::class) + ->setMethods(['processPreSubmit', 'processSubmit', 'processPostSubmit', 'processFinishSubmit']) + ->getMockForAbstractClass(); + + $processor->expects($this->never()) + ->method('processPreSubmit'); + $processor->expects($this->never()) + ->method('processSubmit'); + $processor->expects($this->never()) + ->method('processPostSubmit'); + $processor->expects($this->never()) + ->method('processFinishSubmit'); + + $processor->process($context); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/CustomizeFormDataContextTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/CustomizeFormDataContextTest.php new file mode 100644 index 00000000000..fa3b72389d7 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/CustomizeFormDataContextTest.php @@ -0,0 +1,86 @@ +context = new CustomizeFormDataContext(); + } + + public function testRootClassName() + { + $this->assertNull($this->context->getRootClassName()); + + $this->context->setRootClassName('Test\Class'); + $this->assertEquals('Test\Class', $this->context->getRootClassName()); + } + + public function testClassName() + { + $this->assertNull($this->context->getClassName()); + + $this->context->setClassName('Test\Class'); + $this->assertEquals('Test\Class', $this->context->getClassName()); + } + + public function testPropertyPath() + { + $this->assertNull($this->context->getPropertyPath()); + + $this->context->setPropertyPath('field1.field11'); + $this->assertEquals('field1.field11', $this->context->getPropertyPath()); + } + + public function testRootConfig() + { + $this->assertNull($this->context->getRootConfig()); + + $config = new EntityDefinitionConfig(); + $this->context->setConfig($config); + $this->assertNull($this->context->getRootConfig()); + + $this->context->setPropertyPath('test'); + $this->assertSame($config, $this->context->getRootConfig()); + } + + public function testConfig() + { + $this->assertNull($this->context->getConfig()); + + $config = new EntityDefinitionConfig(); + $config + ->addField('field1') + ->createAndSetTargetEntity() + ->addField('field11') + ->createAndSetTargetEntity(); + + $this->context->setConfig($config); + $this->assertSame($config, $this->context->getConfig()); + + $this->context->setPropertyPath('field1.field11'); + $this->assertSame( + $config->getField('field1')->getTargetEntity()->getField('field11')->getTargetEntity(), + $this->context->getConfig() + ); + + $this->context->setPropertyPath('unknownField.field11'); + $this->assertNull($this->context->getConfig()); + } + + public function testForm() + { + $this->assertNull($this->context->getForm()); + + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $this->context->setForm($form); + $this->assertSame($form, $this->context->getForm()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/MapPrimaryFieldTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/MapPrimaryFieldTest.php new file mode 100644 index 00000000000..75eb547e375 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeFormData/MapPrimaryFieldTest.php @@ -0,0 +1,476 @@ +customizationProcessor = $this->getMock(ActionProcessorInterface::class); + + parent::setUp(); + + $this->dispatcher = new EventDispatcher(); + $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory); + + $configProvider = $this->getMockBuilder('Oro\Bundle\ApiBundle\Provider\ConfigProvider') + ->disableOriginalConstructor() + ->getMock(); + $metadataProvider = $this->getMockBuilder('Oro\Bundle\ApiBundle\Provider\MetadataProvider') + ->disableOriginalConstructor() + ->getMock(); + $this->formContext = new FormContextStub($configProvider, $metadataProvider); + $this->formContext->setVersion('1.1'); + $this->formContext->getRequestType()->add(RequestType::REST); + + $this->processor = new MapPrimaryField( + PropertyAccess::createPropertyAccessor(), + 'Unknown enabled group.', + 'enabledRole', + 'roles', + 'name', + 'enabled' + ); + + $this->customizationProcessor->expects($this->any()) + ->method('createContext') + ->willReturnCallback( + function () { + return new CustomizeFormDataContext(); + } + ); + $this->customizationProcessor->expects($this->any()) + ->method('process') + ->willReturnCallback( + function (CustomizeFormDataContext $context) { + if (Entity\Account::class === $context->getClassName()) { + $this->processor->process($context); + } + } + ); + } + + protected function getExtensions() + { + return [ + new ValidatorExtension(Validation::createValidator()), + new PreloadedExtension( + [], + [FormType::class => [new CustomizeFormDataExtension($this->customizationProcessor)]] + ) + ]; + } + + /** + * @param EntityDefinitionConfig $config + * + * @return FormBuilderInterface + */ + protected function getFormBuilder(EntityDefinitionConfig $config) + { + $this->formContext->setConfig($config); + + return $this->builder->create( + null, + FormType::class, + [ + 'data_class' => Entity\Account::class, + CustomizeFormDataExtension::API_CONTEXT => $this->formContext + ] + ); + } + + /** + * @param EntityDefinitionConfig $config + * @param Entity\Account $data + * @param array $submittedData + * @param array $itemOptions + * @param string $entryType + * + * @return FormInterface + */ + protected function processForm( + EntityDefinitionConfig $config, + Entity\Account $data, + array $submittedData, + array $itemOptions = [], + $entryType = NameContainerType::class + ) { + $formBuilder = $this->getFormBuilder($config); + $formBuilder->add('enabledRole', null, array_merge(['mapped' => false], $itemOptions)); + $formBuilder->add( + 'roles', + 'collection', + [ + 'by_reference' => false, + 'allow_add' => true, + 'allow_delete' => true, + 'entry_type' => $entryType, + 'entry_options' => array_merge(['data_class' => Entity\Role::class], $itemOptions) + ] + ); + + $form = $formBuilder->getForm(); + $form->setData($data); + $form->submit($submittedData, false); + + return $form; + } + + /** + * @param Entity\Account $data + * @param string $name + * @param bool $enabled + * + * @return Entity\Role + */ + protected function addRole(Entity\Account $data, $name, $enabled) + { + $role = new Entity\Role(); + $role->setName($name); + $role->setEnabled($enabled); + $data->addRole($role); + + return $role; + } + + public function testProcessWhenPrimaryFieldIsNotSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $form = $this->processForm($config, $data, []); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $this->assertFalse($role1->isEnabled()); + $this->assertTrue($role2->isEnabled()); + } + + public function testProcessWhenPrimaryFieldIsNotSubmittedButAssociationIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $form = $this->processForm( + $config, + $data, + [ + 'roles' => [ + ['name' => 'role1'], + ['name' => 'role2'], + ] + ] + ); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $this->assertFalse($role1->isEnabled()); + $this->assertTrue($role2->isEnabled()); + } + + public function testProcessWhenEmptyValueForPrimaryFieldIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $form = $this->processForm($config, $data, ['enabledRole' => '']); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $this->assertFalse($role1->isEnabled()); + $this->assertFalse($role2->isEnabled()); + } + + public function testProcessWhenEmptyValueForPrimaryFieldIsSubmittedAndAssociationIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $form = $this->processForm( + $config, + $data, + [ + 'enabledRole' => '', + 'roles' => [ + ['name' => 'role1'], + ['name' => 'role2'], + ] + ] + ); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $this->assertFalse($role1->isEnabled()); + $this->assertFalse($role2->isEnabled()); + } + + public function testProcessWhenPrimaryFieldIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $form = $this->processForm($config, $data, ['enabledRole' => 'role1']); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $this->assertTrue($role1->isEnabled()); + $this->assertFalse($role2->isEnabled()); + } + + public function testProcessWhenBothPrimaryFieldAndAssociationAreSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $form = $this->processForm( + $config, + $data, + [ + 'enabledRole' => 'role1', + 'roles' => [ + ['name' => 'role1'], + ['name' => 'role2'], + ] + ] + ); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $this->assertTrue($role1->isEnabled()); + $this->assertFalse($role2->isEnabled()); + } + + public function testProcessWhenUnknownValueForPrimaryFieldIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $form = $this->processForm($config, $data, ['enabledRole' => 'unknown']); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $roles = $data->getRoles(); + $this->assertCount(3, $roles); + $this->assertFalse($role1->isEnabled()); + $this->assertFalse($role2->isEnabled()); + $this->assertTrue($roles[2]->isEnabled()); + } + + public function testProcessWhenUnknownValueForPrimaryFieldIsSubmittedAndAssociationIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $this->addRole($data, 'role1', false); + $this->addRole($data, 'role2', true); + + $form = $this->processForm( + $config, + $data, + [ + 'enabledRole' => 'unknown', + 'roles' => [ + ['name' => 'role1'], + ['name' => 'role2'], + ] + ] + ); + $this->assertTrue($form->isSynchronized()); + $this->assertFalse($form->isValid()); + /** @var FormError[] $errors */ + $errors = $form->get('enabledRole')->getErrors(); + $this->assertEquals( + 'Unknown enabled group.', + $errors[0]->getMessage() + ); + } + + public function testProcessWhenInvalidValueForPrimaryFieldIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $this->addRole($data, 'role1', false); + $this->addRole($data, 'role2', true); + + $form = $this->processForm( + $config, + $data, + ['enabledRole' => '1'], + ['constraints' => [new Assert\Length(['min' => 3])]], + RestrictedNameContainerType::class + ); + $this->assertTrue($form->isSynchronized()); + $this->assertFalse($form->isValid()); + /** @var FormError[] $errors */ + $errors = $form->get('enabledRole')->getErrors(); + $this->assertEquals( + 'This value is too short. It should have 3 characters or more.', + $errors[0]->getMessage() + ); + $this->assertCount(0, $form->get('roles')->get('2')->getErrors(true)); + } + + public function testProcessWhenInvalidValueForPrimaryFieldIsSubmittedAndAssociationIsSubmitted() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('roles'); + $rolesField->getOrCreateTargetEntity()->addField('name'); + + $data = new Entity\Account(); + $this->addRole($data, 'role1', false); + $this->addRole($data, 'role2', true); + + $form = $this->processForm( + $config, + $data, + [ + 'enabledRole' => '1', + 'roles' => [ + ['name' => 'role1'], + ['name' => 'role2'], + ] + ], + ['constraints' => [new Assert\Length(['min' => 3])]] + ); + $this->assertTrue($form->isSynchronized()); + $this->assertFalse($form->isValid()); + /** @var FormError[] $errors */ + $errors = $form->get('enabledRole')->getErrors(); + $this->assertEquals( + 'Unknown enabled group.', + $errors[0]->getMessage() + ); + } + + public function testProcessForRenamedFields() + { + $this->processor = new MapPrimaryField( + PropertyAccess::createPropertyAccessor(), + 'Unknown enabled group.', + 'enabledRole', + 'renamedRoles', + 'renamedName', + 'enabled' + ); + + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('renamedRoles'); + $rolesField->setPropertyPath('roles'); + $rolesField->getOrCreateTargetEntity()->addField('renamedName')->setPropertyPath('name'); + + $data = new Entity\Account(); + $role1 = $this->addRole($data, 'role1', false); + $role2 = $this->addRole($data, 'role2', true); + + $formBuilder = $this->getFormBuilder($config); + $formBuilder->add('enabledRole', null, ['mapped' => false]); + $formBuilder->add( + 'renamedRoles', + 'collection', + [ + 'property_path' => 'roles', + 'by_reference' => false, + 'allow_add' => true, + 'allow_delete' => true, + 'entry_type' => RenamedNameContainerType::class, + 'entry_options' => ['data_class' => Entity\Role::class] + ] + ); + + $form = $formBuilder->getForm(); + $form->setData($data); + $form->submit(['enabledRole' => 'role1'], false); + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + $this->assertTrue($role1->isEnabled()); + $this->assertFalse($role2->isEnabled()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedData/ComputePrimaryFieldTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedData/ComputePrimaryFieldTest.php new file mode 100644 index 00000000000..7f5ff52e0dd --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedData/ComputePrimaryFieldTest.php @@ -0,0 +1,187 @@ +context = new CustomizeLoadedDataContext(); + $this->processor = new ComputePrimaryField( + 'enabledRole', + 'roles', + 'name', + 'enabled' + ); + } + + public function testProcessWhenNoData() + { + $this->processor->process($this->context); + $this->assertFalse($this->context->hasResult()); + } + + public function testProcessWhenNoConfigForPrimaryField() + { + $config = new EntityDefinitionConfig(); + + $this->context->setResult( + [ + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ] + ); + $this->context->setConfig($config); + $this->processor->process($this->context); + $this->assertEquals( + [ + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ], + $this->context->getResult() + ); + } + + public function testProcessForExcludedPrimaryField() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole')->setExcluded(); + $rolesConfig = $config->addField('roles')->getOrCreateTargetEntity(); + $rolesConfig->addField('name'); + $rolesConfig->addField('enabled'); + + $this->context->setResult( + [ + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ] + ); + $this->context->setConfig($config); + $this->processor->process($this->context); + $this->assertEquals( + [ + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ], + $this->context->getResult() + ); + } + + public function testProcessWhenPrimaryFieldIsAlreadySet() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesConfig = $config->addField('roles')->getOrCreateTargetEntity(); + $rolesConfig->addField('name'); + $rolesConfig->addField('enabled'); + + $this->context->setResult( + [ + 'enabledRole' => 'role1', + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ] + ); + $this->context->setConfig($config); + $this->processor->process($this->context); + $this->assertEquals( + [ + 'enabledRole' => 'role1', + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ], + $this->context->getResult() + ); + } + + public function testProcessWhenPrimaryFieldIsNotSetYet() + { + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesConfig = $config->addField('roles')->getOrCreateTargetEntity(); + $rolesConfig->addField('name'); + $rolesConfig->addField('enabled'); + + $this->context->setResult( + [ + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ] + ); + $this->context->setConfig($config); + $this->processor->process($this->context); + $this->assertEquals( + [ + 'enabledRole' => 'role2', + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ], + $this->context->getResult() + ); + } + + public function testProcessForRenamedFields() + { + $this->processor = new ComputePrimaryField( + 'enabledRole', + 'renamedRoles', + 'renamedName', + 'renamedEnabled' + ); + + $config = new EntityDefinitionConfig(); + $config->addField('enabledRole'); + $rolesField = $config->addField('renamedRoles'); + $rolesField->setPropertyPath('roles'); + $rolesConfig = $rolesField->getOrCreateTargetEntity(); + $rolesConfig->addField('renamedName')->setPropertyPath('name'); + $rolesConfig->addField('renamedEnabled')->setPropertyPath('enabled'); + + $this->context->setResult( + [ + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ] + ); + $this->context->setConfig($config); + $this->processor->process($this->context); + $this->assertEquals( + [ + 'enabledRole' => 'role2', + 'roles' => [ + ['name' => 'role1', 'enabled' => false], + ['name' => 'role2', 'enabled' => true] + ] + ], + $this->context->getResult() + ); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedDataContextTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedData/CustomizeLoadedDataContextTest.php similarity index 93% rename from src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedDataContextTest.php rename to src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedData/CustomizeLoadedDataContextTest.php index 7187ddeb3ba..dd1446646fd 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedDataContextTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/CustomizeLoadedData/CustomizeLoadedDataContextTest.php @@ -1,9 +1,9 @@ setName($associationName); @@ -108,6 +113,7 @@ protected function createAssociationMetadata( $associationMetadata->setDataType($dataType); $associationMetadata->setAcceptableTargetClassNames($acceptableTargetClasses); $associationMetadata->setIsNullable(true); + $associationMetadata->setCollapsed($collapsed); return $associationMetadata; } @@ -220,6 +226,96 @@ public function testProcessForNotManageableEntity() $this->assertEquals($expectedMetadata, $this->context->getResult()); } + public function testProcessForNotManageableEntityWithExcludedProperties() + { + $config = [ + 'exclusion_policy' => 'all', + 'identifier_field_names' => ['field1'], + 'fields' => [ + 'field1' => [ + 'data_type' => 'string' + ], + 'field2' => [ + 'data_type' => 'string', + 'exclude' => true + ], + ] + ]; + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(false); + + $this->context->setConfig($this->createConfigObject($config)); + $this->context->setWithExcludedProperties(true); + $this->processor->process($this->context); + + $this->assertNotNull($this->context->getResult()); + + $expectedMetadata = new EntityMetadata(); + $expectedMetadata->setClassName(self::TEST_CLASS_NAME); + $expectedMetadata->setIdentifierFieldNames(['field1']); + $expectedMetadata->addField($this->createFieldMetadata('field1', 'string'))->setIsNullable(false); + $expectedMetadata->addField($this->createFieldMetadata('field2', 'string'))->setIsNullable(true); + + $this->assertEquals($expectedMetadata, $this->context->getResult()); + } + + public function testProcessCollapsedArrayAssociationForNotManageableEntity() + { + $config = [ + 'exclusion_policy' => 'all', + 'identifier_field_names' => ['field1'], + 'fields' => [ + 'field1' => [ + 'data_type' => 'integer' + ], + 'association1' => [ + 'data_type' => 'array', + 'exclusion_policy' => 'all', + 'collapsed' => true, + 'target_class' => 'Test\Association1Target', + 'target_type' => 'to-many', + 'identifier_field_names' => ['id'], + 'fields' => [ + 'name' => [ + 'data_type' => 'string' + ] + ] + ], + ] + ]; + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(false); + + $this->context->setConfig($this->createConfigObject($config)); + $this->processor->process($this->context); + + $this->assertNotNull($this->context->getResult()); + + $expectedMetadata = new EntityMetadata(); + $expectedMetadata->setClassName(self::TEST_CLASS_NAME); + $expectedMetadata->setIdentifierFieldNames(['field1']); + $expectedMetadata->addField($this->createFieldMetadata('field1', 'integer'))->setIsNullable(false); + $expectedMetadata->addAssociation( + $this->createAssociationMetadata( + 'association1', + 'Test\Association1Target', + 'manyToMany', + true, + 'array', + ['Test\Association1Target'], + true + ) + ); + + $this->assertEquals($expectedMetadata, $this->context->getResult()); + } + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -393,6 +489,85 @@ public function testProcessForManageableEntityWithConfig() $this->assertEquals($expectedMetadata, $this->context->getResult()); } + public function testProcessForManageableEntityWithExcludedProperties() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => null, + 'field2' => [ + 'exclude' => true + ], + 'metaProperty1' => [ + 'meta_property' => true + ], + 'metaProperty2' => [ + 'meta_property' => true, + 'exclude' => true + ], + ] + ]; + + $classMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $classMetadata->expects($this->once()) + ->method('getIdentifierFieldNames') + ->willReturn(['field1']); + $classMetadata->expects($this->once()) + ->method('usesIdGenerator') + ->willReturn(true); + + $classMetadata->expects($this->once()) + ->method('getFieldNames') + ->willReturn( + [ + 'field1', + 'field2', + 'metaProperty1', + 'metaProperty2', + ] + ); + $classMetadata->expects($this->exactly(4)) + ->method('getTypeOfField') + ->willReturnMap( + [ + ['field1', 'string'], + ['field2', 'string'], + ['metaProperty1', 'string'], + ['metaProperty2', 'string'], + ] + ); + $classMetadata->expects($this->once()) + ->method('getAssociationNames') + ->willReturn([]); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->once()) + ->method('getEntityMetadataForClass') + ->with(self::TEST_CLASS_NAME, true) + ->willReturn($classMetadata); + + $this->context->setConfig($this->createConfigObject($config)); + $this->context->setWithExcludedProperties(true); + $this->processor->process($this->context); + + $this->assertNotNull($this->context->getResult()); + + $expectedMetadata = new EntityMetadata(); + $expectedMetadata->setClassName(self::TEST_CLASS_NAME); + $expectedMetadata->setInheritedType(false); + $expectedMetadata->setIdentifierFieldNames(['field1']); + $expectedMetadata->setHasIdentifierGenerator(true); + $expectedMetadata->addField($this->createFieldMetadata('field1', 'string')); + $expectedMetadata->addField($this->createFieldMetadata('field2', 'string')); + $expectedMetadata->addMetaProperty($this->createMetaPropertyMetadata('metaProperty1', 'string')); + $expectedMetadata->addMetaProperty($this->createMetaPropertyMetadata('metaProperty2', 'string')); + + $this->assertEquals($expectedMetadata, $this->context->getResult()); + } + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -848,6 +1023,98 @@ public function testProcessForManageableEntityWithNotManageableAssociationWithou $this->assertEquals($expectedMetadata, $this->context->getResult()); } + public function testProcessCollapsedArrayAssociationForManageableEntity() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'id' => null, + 'association1' => [ + 'data_type' => 'array', + 'exclusion_policy' => 'all', + 'collapsed' => true, + 'fields' => [ + 'name' => null + ] + ], + ] + ]; + + $classMetadata = $this->getClassMetadataMock(self::TEST_CLASS_NAME); + $classMetadata->expects($this->once()) + ->method('getIdentifierFieldNames') + ->willReturn(['id']); + $classMetadata->expects($this->once()) + ->method('usesIdGenerator') + ->willReturn(true); + + $classMetadata->expects($this->once()) + ->method('getFieldNames') + ->willReturn(['id']); + $classMetadata->expects($this->once()) + ->method('getTypeOfField') + ->with('id') + ->willReturn('integer'); + $classMetadata->expects($this->once()) + ->method('getAssociationNames') + ->willReturn(['association1']); + $classMetadata->expects($this->once()) + ->method('getAssociationTargetClass') + ->with('association1') + ->willReturn('Test\Association1Target'); + $classMetadata->expects($this->once()) + ->method('isCollectionValuedAssociation') + ->with('association1') + ->willReturn(true); + $classMetadata->expects($this->once()) + ->method('getAssociationMapping') + ->with('association1') + ->willReturn(['type' => ClassMetadata::MANY_TO_MANY]); + + $association1ClassMetadata = $this->getClassMetadataMock('Test\Association1Target'); + $association1ClassMetadata->expects($this->never()) + ->method('getIdentifierFieldNames'); + $association1ClassMetadata->expects($this->never()) + ->method('getTypeOfField'); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->exactly(2)) + ->method('getEntityMetadataForClass') + ->willReturnMap( + [ + [self::TEST_CLASS_NAME, true, $classMetadata], + ['Test\Association1Target', true, $association1ClassMetadata], + ] + ); + + $this->context->setConfig($this->createConfigObject($config)); + $this->processor->process($this->context); + + $this->assertNotNull($this->context->getResult()); + + $expectedMetadata = new EntityMetadata(); + $expectedMetadata->setClassName(self::TEST_CLASS_NAME); + $expectedMetadata->setIdentifierFieldNames(['id']); + $expectedMetadata->setHasIdentifierGenerator(true); + $expectedMetadata->addField($this->createFieldMetadata('id', 'integer'))->setIsNullable(false); + $expectedMetadata->addAssociation( + $this->createAssociationMetadata( + 'association1', + 'Test\Association1Target', + 'manyToMany', + true, + 'array', + ['Test\Association1Target'], + true + ) + ); + + $this->assertEquals($expectedMetadata, $this->context->getResult()); + } + public function testProcessForExtendedAssociation() { $config = [ @@ -922,7 +1189,8 @@ public function testProcessForExtendedAssociation() 'manyToOne', false, 'string', - ['Test\Association1Target'] + ['Test\Association1Target'], + true ) ); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/MetadataContextTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/MetadataContextTest.php index 3c950c757c1..387d70313a0 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/MetadataContextTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/MetadataContextTest.php @@ -77,4 +77,13 @@ public function testSetInvalidExtras() { $this->context->setExtras([new \stdClass()]); } + + public function testWithExcludedProperties() + { + $this->assertFalse($this->context->getWithExcludedProperties()); + + $this->context->setWithExcludedProperties(true); + $this->assertTrue($this->context->getWithExcludedProperties()); + $this->assertTrue($this->context->get(MetadataContext::WITH_EXCLUDED_PROPERTIES)); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/NormalizeMetadataTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/NormalizeMetadataTest.php index c746337189d..61ccaf466ba 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/NormalizeMetadataTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/GetMetadata/NormalizeMetadataTest.php @@ -66,6 +66,7 @@ protected function createFieldMetadata($fieldName, $dataType = null) * @param bool|null $isCollection * @param string|null $dataType * @param string[]|null $acceptableTargetClasses + * @param bool $collapsed * * @return AssociationMetadata */ @@ -75,7 +76,8 @@ protected function createAssociationMetadata( $associationType = null, $isCollection = null, $dataType = null, - $acceptableTargetClasses = null + $acceptableTargetClasses = null, + $collapsed = false ) { $associationMetadata = new AssociationMetadata(); $associationMetadata->setName($associationName); @@ -93,6 +95,7 @@ protected function createAssociationMetadata( $associationMetadata->setAcceptableTargetClassNames($acceptableTargetClasses); } $associationMetadata->setIsNullable(false); + $associationMetadata->setCollapsed($collapsed); return $associationMetadata; } @@ -178,6 +181,39 @@ public function testProcessNormalizationWithoutLinkedProperties() $this->assertEquals($expectedMetadata, $this->context->getResult()); } + public function testProcessWhenExcludedPropertiesShouldNotBeRemoved() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => null, + 'field2' => [ + 'exclude' => true + ], + ] + ]; + + $metadata = new EntityMetadata(); + $metadata->addField($this->createFieldMetadata('field1')); + $metadata->addField($this->createFieldMetadata('field2')); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + + $this->context->setConfig($this->createConfigObject($config)); + $this->context->setResult($metadata); + $this->context->setWithExcludedProperties(true); + $this->processor->process($this->context); + + $expectedMetadata = new EntityMetadata(); + $expectedMetadata->addField($this->createFieldMetadata('field1')); + $expectedMetadata->addField($this->createFieldMetadata('field2')); + + $this->assertEquals($expectedMetadata, $this->context->getResult()); + } + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -282,20 +318,17 @@ public function testProcessLinkedProperties() ->with('Test\Association411Target') ->willReturn($association411ClassMetadata); - $this->metadataProvider->expects($this->exactly(1)) + $this->metadataProvider->expects($this->once()) ->method('getMetadata') - ->willReturnMap( - [ - [ - 'Test\Association411Target', - $this->context->getVersion(), - $this->context->getRequestType(), - $configObject->getField('association4')->getTargetEntity(), - $this->context->getExtras(), - $association411TargetMetadata - ] - ] - ); + ->with( + 'Test\Association411Target', + $this->context->getVersion(), + $this->context->getRequestType(), + $configObject->getField('association4')->getTargetEntity(), + $this->context->getExtras(), + false + ) + ->willReturn($association411TargetMetadata); $this->context->setConfig($configObject); $this->context->setResult($metadata); @@ -410,24 +443,21 @@ public function testProcessRenamedLinkedProperty() ->with('Test\Association11Target') ->willReturn($association11ClassMetadata); - $this->metadataProvider->expects($this->exactly(1)) + $this->metadataProvider->expects($this->once()) ->method('getMetadata') - ->willReturnMap( - [ - [ - 'Test\Association11Target', - $this->context->getVersion(), - $this->context->getRequestType(), - $configObject - ->getField('association1') - ->getTargetEntity() - ->getField('association11') - ->getTargetEntity(), - $this->context->getExtras(), - $association11TargetMetadata - ] - ] - ); + ->with( + 'Test\Association11Target', + $this->context->getVersion(), + $this->context->getRequestType(), + $configObject + ->getField('association1') + ->getTargetEntity() + ->getField('association11') + ->getTargetEntity(), + $this->context->getExtras(), + false + ) + ->willReturn($association11TargetMetadata); $this->context->setConfig($configObject); $this->context->setResult($metadata); @@ -449,4 +479,128 @@ public function testProcessRenamedLinkedProperty() $this->assertEquals($expectedMetadata, $this->context->getResult()); } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProcessCollapsedArrayAssociationLinkedProperty() + { + $config = [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'linkedAssociation1' => [ + 'property_path' => 'realAssociation1.realAssociation11', + ], + 'association1' => [ + 'exclude' => true, + 'property_path' => 'realAssociation1', + 'fields' => [ + 'association11' => [ + 'data_type' => 'array', + 'collapse' => true, + 'exclusion_policy' => 'all', + 'property_path' => 'realAssociation11', + 'fields' => [ + 'name' => null + ] + ] + ] + ] + ] + ]; + $configObject = $this->createConfigObject($config); + + $metadata = new EntityMetadata(); + $metadata->setClassName(self::TEST_CLASS_NAME); + $metadata->addAssociation( + $this->createAssociationMetadata( + 'association1', + 'Test\Association1Target', + 'manyToOne', + false, + 'integer', + ['Test\Association1Target'] + ) + ); + + $association1ClassMetadata = $this->getClassMetadataMock('Test\Association1Target'); + $association1ClassMetadata->expects($this->once()) + ->method('hasAssociation') + ->with('realAssociation11') + ->willReturn(true); + $association1ClassMetadata->expects($this->once()) + ->method('getAssociationTargetClass') + ->with('realAssociation11') + ->willReturn('Test\Association11Target'); + $association1ClassMetadata->expects($this->once()) + ->method('isCollectionValuedAssociation') + ->with('realAssociation11') + ->willReturn(true); + $association1ClassMetadata->expects($this->once()) + ->method('getAssociationMapping') + ->with('realAssociation11') + ->willReturn(['type' => ClassMetadata::MANY_TO_MANY]); + + $association11ClassMetadata = $this->getClassMetadataMock('Test\Association11Target'); + $association11ClassMetadata->expects($this->once()) + ->method('getIdentifierFieldNames') + ->willReturn(['id']); + $association11ClassMetadata->expects($this->once()) + ->method('getTypeOfField') + ->with('id') + ->willReturn('integer'); + + $association11TargetMetadata = new EntityMetadata(); + $association11TargetMetadata->setClassName('Test\Association11Target'); + + $this->doctrineHelper->expects($this->once()) + ->method('isManageableEntityClass') + ->with(self::TEST_CLASS_NAME) + ->willReturn(true); + $this->doctrineHelper->expects($this->once()) + ->method('findEntityMetadataByPath') + ->with(self::TEST_CLASS_NAME, ['realAssociation1']) + ->willReturn($association1ClassMetadata); + $this->doctrineHelper->expects($this->once()) + ->method('getEntityMetadataForClass') + ->with('Test\Association11Target') + ->willReturn($association11ClassMetadata); + + $this->metadataProvider->expects($this->once()) + ->method('getMetadata') + ->with( + 'Test\Association11Target', + $this->context->getVersion(), + $this->context->getRequestType(), + $configObject + ->getField('association1') + ->getTargetEntity() + ->getField('association11') + ->getTargetEntity(), + $this->context->getExtras(), + false + ) + ->willReturn($association11TargetMetadata); + + $this->context->setConfig($configObject); + $this->context->setResult($metadata); + $this->processor->process($this->context); + + $expectedMetadata = new EntityMetadata(); + $expectedMetadata->setClassName(self::TEST_CLASS_NAME); + $expectedLinkedAssociation1 = $this->createAssociationMetadata( + 'linkedAssociation1', + 'Test\Association11Target', + 'manyToMany', + true, + 'array', + ['Test\Association11Target'], + true + ); + $expectedLinkedAssociation1->setIsNullable(true); + $expectedLinkedAssociation1->setTargetMetadata($association11TargetMetadata); + $expectedMetadata->addAssociation($expectedLinkedAssociation1); + + $this->assertEquals($expectedMetadata, $this->context->getResult()); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildCriteriaTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildCriteriaTest.php index 12fbcb1fc3f..6306f46c680 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildCriteriaTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildCriteriaTest.php @@ -6,30 +6,65 @@ use Doctrine\Common\Collections\Expr\CompositeExpression; use Oro\Bundle\ApiBundle\Collection\Criteria; -use Oro\Bundle\ApiBundle\Config\Config; -use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; -use Oro\Bundle\ApiBundle\Config\FiltersConfig; -use Oro\Bundle\ApiBundle\Model\Error; -use Oro\Bundle\ApiBundle\Model\ErrorSource; +use Oro\Bundle\ApiBundle\Filter\ComparisonFilter; use Oro\Bundle\ApiBundle\Processor\Shared\BuildCriteria; -use Oro\Bundle\ApiBundle\Request\Constraint; use Oro\Bundle\ApiBundle\Request\RestFilterValueAccessor; use Oro\Bundle\ApiBundle\Tests\Unit\Processor\GetList\GetListProcessorOrmRelatedTestCase; class BuildCriteriaTest extends GetListProcessorOrmRelatedTestCase { - const ENTITY_NAMESPACE = 'Oro\Bundle\ApiBundle\Tests\Unit\Fixtures\Entity\\'; - - /** @var BuildCriteria */ + /** @var BuildCriteria */ protected $processor; protected function setUp() { parent::setUp(); - $this->context->setAction('get_list'); + $this->processor = new BuildCriteria(); + } + + /** + * @param $queryString + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getRequest($queryString) + { + $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + $request->expects($this->any()) + ->method('getQueryString') + ->willReturn($queryString); + + return $request; + } + + /** + * @return Criteria + */ + protected function getCriteria() + { + $resolver = $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\EntityClassResolver') + ->disableOriginalConstructor() + ->getMock(); + + return new Criteria($resolver); + } + + /** + * @param string $dataType + * @param string $propertyPath + * + * @return ComparisonFilter + */ + protected function getComparisonFilter($dataType, $propertyPath) + { + $filter = new ComparisonFilter($dataType); + $filter->setSupportedOperators(['=', '!=']); + $filter->setField($propertyPath); - $this->processor = new BuildCriteria($this->configProvider, $this->doctrineHelper); + return $filter; } public function testProcessWhenQueryIsAlreadyBuilt() @@ -51,22 +86,16 @@ public function testProcessWhenCriteriaObjectDoesNotExist() $this->assertFalse($this->context->hasQuery()); } - public function testProcessFilteringByPrimaryEntityFields() + public function testProcess() { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['name', 'label']); - $primaryEntityFilters = $this->getFiltersConfig(['name' => 'string', 'label' => 'string']); - $request = $this->getRequest('filter[label]=val1&filter[name]=val2'); - $this->configProvider->expects($this->never()) - ->method('getConfig'); + $filers = $this->context->getFilters(); + $filers->add('filter[label]', $this->getComparisonFilter('string', 'label')); + $filers->add('filter[name]', $this->getComparisonFilter('string', 'association.name')); - $this->context->setClassName($this->getEntityClass('Category')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); $this->context->setFilterValues(new RestFilterValueAccessor($request)); $this->context->setCriteria($this->getCriteria()); - $this->processor->process($this->context); $this->assertEquals( @@ -74,321 +103,23 @@ public function testProcessFilteringByPrimaryEntityFields() 'AND', [ new Comparison('label', '=', 'val1'), - new Comparison('name', '=', 'val2') + new Comparison('association.name', '=', 'val2') ] ), $this->context->getCriteria()->getWhereExpression() ); - $this->assertCount(0, $this->context->getErrors()); } - public function testProcessFilteringByUnknownPrimaryEntityField() + public function testProcessForUnknownFilter() { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['name', 'label']); - $primaryEntityFilters = $this->getFiltersConfig(['name' => 'string', 'label' => 'string']); - - $request = $this->getRequest('filter[label1]=test'); - - $this->configProvider->expects($this->never()) - ->method('getConfig'); + $request = $this->getRequest('filter[name]=val1'); - $this->context->setClassName($this->getEntityClass('Category')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); $this->context->setFilterValues(new RestFilterValueAccessor($request)); $this->context->setCriteria($this->getCriteria()); - $this->processor->process($this->context); $this->assertNull( $this->context->getCriteria()->getWhereExpression() ); - $this->assertEquals( - [ - Error::createValidationError( - Constraint::FILTER, - sprintf('Filter "%s" is not supported.', 'filter[label1]') - )->setSource(ErrorSource::createByParameter('filter[label1]')) - ], - $this->context->getErrors() - ); - } - - public function testProcessFilteringByPrimaryEntityFieldWhichCannotBuUsedForFiltering() - { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['name', 'label']); - $primaryEntityFilters = $this->getFiltersConfig(['name' => 'string']); - - $request = $this->getRequest('filter[label]=test'); - - $this->configProvider->expects($this->never()) - ->method('getConfig'); - - $this->context->setClassName($this->getEntityClass('Category')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); - $this->context->setFilterValues(new RestFilterValueAccessor($request)); - $this->context->setCriteria($this->getCriteria()); - - $this->processor->process($this->context); - - $this->assertNull( - $this->context->getCriteria()->getWhereExpression() - ); - $this->assertEquals( - [ - Error::createValidationError( - Constraint::FILTER, - sprintf('Filter "%s" is not supported.', 'filter[label]') - )->setSource(ErrorSource::createByParameter('filter[label]')) - ], - $this->context->getErrors() - ); - } - - public function testProcessFilteringByRelatedEntityField() - { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category']); - $primaryEntityFilters = $this->getFiltersConfig(); - - $request = $this->getRequest('filter[category.name]=test'); - - $this->configProvider->expects($this->once()) - ->method('getConfig') - ->willReturn( - $this->getConfig( - ['name'], - ['name' => 'string'] - ) - ); - - $this->context->setClassName($this->getEntityClass('User')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); - $this->context->setFilterValues(new RestFilterValueAccessor($request)); - $this->context->setCriteria($this->getCriteria()); - - $this->processor->process($this->context); - - $this->assertEquals( - new Comparison('category.name', '=', 'test'), - $this->context->getCriteria()->getWhereExpression() - ); - $this->assertCount(0, $this->context->getErrors()); - } - - public function testProcessFilteringByRelatedEntityFieldWhenAssociationDoesNotExist() - { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category']); - $primaryEntityFilters = $this->getFiltersConfig(); - - $request = $this->getRequest('filter[category1.name]=test'); - - $this->configProvider->expects($this->never()) - ->method('getConfig'); - - $this->context->setClassName($this->getEntityClass('User')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); - $this->context->setFilterValues(new RestFilterValueAccessor($request)); - $this->context->setCriteria($this->getCriteria()); - - $this->processor->process($this->context); - - $this->assertNull( - $this->context->getCriteria()->getWhereExpression() - ); - $this->assertEquals( - [ - Error::createValidationError( - Constraint::FILTER, - sprintf('Filter "%s" is not supported.', 'filter[category1.name]') - )->setSource(ErrorSource::createByParameter('filter[category1.name]')) - ], - $this->context->getErrors() - ); - } - - public function testProcessFilteringByRelatedEntityFieldWhenAssociationIsRenamed() - { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category1']); - $primaryEntityConfig->getField('category1')->setPropertyPath('category'); - $primaryEntityFilters = $this->getFiltersConfig(); - - $request = $this->getRequest('filter[category1.name]=test'); - - $this->configProvider->expects($this->once()) - ->method('getConfig') - ->willReturn( - $this->getConfig( - ['name'], - ['name' => 'string'] - ) - ); - - $this->context->setClassName($this->getEntityClass('User')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); - $this->context->setFilterValues(new RestFilterValueAccessor($request)); - $this->context->setCriteria($this->getCriteria()); - - $this->processor->process($this->context); - - $this->assertEquals( - new Comparison('category.name', '=', 'test'), - $this->context->getCriteria()->getWhereExpression() - ); - $this->assertCount(0, $this->context->getErrors()); - } - - public function testProcessFilteringByRenamedRelatedEntityField() - { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category']); - $primaryEntityFilters = $this->getFiltersConfig(); - - $request = $this->getRequest('filter[category.name1]=test'); - - $categoryConfig = $this->getConfig( - ['name1'], - ['name1' => 'string'] - ); - $categoryConfig->getDefinition()->getField('name1')->setPropertyPath('name'); - $categoryConfig->getFilters()->getField('name1')->setPropertyPath('name'); - - $this->configProvider->expects($this->once()) - ->method('getConfig') - ->willReturn($categoryConfig); - - $this->context->setClassName($this->getEntityClass('User')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); - $this->context->setFilterValues(new RestFilterValueAccessor($request)); - $this->context->setCriteria($this->getCriteria()); - - $this->processor->process($this->context); - - $this->assertEquals( - new Comparison('category.name', '=', 'test'), - $this->context->getCriteria()->getWhereExpression() - ); - $this->assertCount(0, $this->context->getErrors()); - } - - public function testProcessFilteringByRenamedAssociationAndRenamedRelatedEntityField() - { - $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category1']); - $primaryEntityConfig->getField('category1')->setPropertyPath('category'); - $primaryEntityFilters = $this->getFiltersConfig(); - - $request = $this->getRequest('filter[category1.name1]=test'); - - $categoryConfig = $this->getConfig( - ['name1'], - ['name1' => 'string'] - ); - $categoryConfig->getDefinition()->getField('name1')->setPropertyPath('name'); - $categoryConfig->getFilters()->getField('name1')->setPropertyPath('name'); - - $this->configProvider->expects($this->once()) - ->method('getConfig') - ->willReturn($categoryConfig); - - $this->context->setClassName($this->getEntityClass('User')); - $this->context->setConfig($primaryEntityConfig); - $this->context->setConfigOfFilters($primaryEntityFilters); - $this->context->setFilterValues(new RestFilterValueAccessor($request)); - $this->context->setCriteria($this->getCriteria()); - - $this->processor->process($this->context); - - $this->assertEquals( - new Comparison('category.name', '=', 'test'), - $this->context->getCriteria()->getWhereExpression() - ); - $this->assertCount(0, $this->context->getErrors()); - } - - /** - * @param string $entityShortClass - * - * @return string - */ - protected function getEntityClass($entityShortClass) - { - return self::ENTITY_NAMESPACE . $entityShortClass; - } - - /** - * @param string[] $fields - * @param array $filterFields - * - * @return Config - */ - protected function getConfig(array $fields = [], array $filterFields = []) - { - $config = new Config(); - $config->setDefinition($this->getEntityDefinitionConfig($fields)); - $config->setFilters($this->getFiltersConfig($filterFields)); - - return $config; - } - - /** - * @param string[] $fields - * - * @return EntityDefinitionConfig - */ - protected function getEntityDefinitionConfig(array $fields = []) - { - $config = new EntityDefinitionConfig(); - foreach ($fields as $field) { - $config->addField($field); - } - - return $config; - } - - /** - * @param array $filterFields - * - * @return FiltersConfig - */ - protected function getFiltersConfig(array $filterFields = []) - { - $config = new FiltersConfig(); - foreach ($filterFields as $field => $dataType) { - $config->addField($field)->setDataType($dataType); - } - - return $config; - } - - /** - * @param $queryString - * - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getRequest($queryString) - { - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') - ->disableOriginalConstructor() - ->getMock(); - $request->expects($this->once()) - ->method('getQueryString') - ->willReturn($queryString); - - return $request; - } - - /** - * @return Criteria - */ - protected function getCriteria() - { - $resolver = $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\EntityClassResolver') - ->disableOriginalConstructor() - ->getMock(); - - return new Criteria($resolver); } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildFormBuilderTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildFormBuilderTest.php index 73125207c87..b42f4e9da01 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildFormBuilderTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/BuildFormBuilderTest.php @@ -8,6 +8,7 @@ use Oro\Bundle\ApiBundle\Metadata\FieldMetadata; use Oro\Bundle\ApiBundle\Processor\Shared\BuildFormBuilder; use Oro\Bundle\ApiBundle\Tests\Unit\Processor\FormProcessorTestCase; +use Oro\Bundle\ApiBundle\Util\ConfigUtil; class BuildFormBuilderTest extends FormProcessorTestCase { @@ -90,7 +91,8 @@ public function testProcessForCustomForm() [ 'data_class' => $entityClass, 'validation_groups' => ['Default', 'api', 'my_group'], - 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"' + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'api_context' => $this->context ] ) ->willReturn($formBuilder); @@ -143,7 +145,8 @@ public function testProcess() [ 'data_class' => $entityClass, 'validation_groups' => ['Default', 'api'], - 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"' + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'api_context' => $this->context ] ) ->willReturn($formBuilder); @@ -198,4 +201,47 @@ public function testProcess() $this->processor->process($this->context); $this->assertSame($formBuilder, $this->context->getFormBuilder()); } + + public function testProcessForIgnoredField() + { + $entityClass = 'Test\Entity'; + $data = new \stdClass(); + $formBuilder = $this->getMock('Symfony\Component\Form\FormBuilderInterface'); + + $config = new EntityDefinitionConfig(); + $config->addField('field1')->setPropertyPath(ConfigUtil::IGNORE_PROPERTY_PATH); + + $metadata = new EntityMetadata(); + $metadata->addField($this->createFieldMetadata('field1')); + + $this->formFactory->expects($this->once()) + ->method('createNamedBuilder') + ->with( + null, + 'form', + $data, + [ + 'data_class' => $entityClass, + 'validation_groups' => ['Default', 'api'], + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'api_context' => $this->context + ] + ) + ->willReturn($formBuilder); + + $formBuilder->expects($this->once()) + ->method('add') + ->with( + 'field1', + null, + ['mapped' => false] + ); + + $this->context->setClassName($entityClass); + $this->context->setConfig($config); + $this->context->setMetadata($metadata); + $this->context->setResult($data); + $this->processor->process($this->context); + $this->assertSame($formBuilder, $this->context->getFormBuilder()); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php index 27a50a56893..87648505d46 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php @@ -138,6 +138,34 @@ public function testProcessWithPropertyWhichDoesNotRegisteredInFormAndHasInvalid ); } + public function testProcessWithInvalidRenamedPropertyValue() + { + $dataClass = 'Oro\Bundle\ApiBundle\Tests\Unit\Fixtures\FormValidation\TestObject'; + $data = new $dataClass(); + + $form = $this->createFormBuilder()->create('testForm', null, ['compound' => true, 'data_class' => $dataClass]) + ->add('renamedTitle', 'text', ['property_path' => 'title']) + ->getForm(); + $form->setData($data); + $form->submit( + [ + 'renamedTitle' => null, + ] + ); + + $this->context->setForm($form); + $this->processor->process($this->context); + + $this->assertFalse($form->isValid()); + $this->assertTrue($this->context->hasErrors()); + $this->assertEquals( + [ + $this->createErrorObject('not blank constraint', 'This value should not be blank.', 'renamedTitle'), + ], + $this->context->getErrors() + ); + } + public function testProcessWithInvalidPropertyValues() { $form = $this->createFormBuilder()->create('testForm', null, ['compound' => true]) @@ -194,6 +222,34 @@ public function testProcessWithInvalidCollectionPropertyValue() ); } + public function testProcessWithInvalidCollectionRenamedPropertyValue() + { + $form = $this->createFormBuilder()->create('testForm', null, ['compound' => true]) + ->add( + 'renamedField1', + 'text', + ['property_path' => '[field1]', 'constraints' => [new Constraints\All(new Constraints\NotNull())]] + ) + ->getForm(); + $form->submit( + [ + 'renamedField1' => [1, null], + ] + ); + + $this->context->setForm($form); + $this->processor->process($this->context); + + $this->assertFalse($form->isValid()); + $this->assertTrue($this->context->hasErrors()); + $this->assertEquals( + [ + $this->createErrorObject('not null constraint', 'This value should not be null.', 'renamedField1.1') + ], + $this->context->getErrors() + ); + } + public function testProcessWithInvalidCollectionPropertyValueWhenFormFieldIsCollectionType() { $form = $this->createFormBuilder()->create('testForm', null, ['compound' => true]) @@ -226,6 +282,39 @@ public function testProcessWithInvalidCollectionPropertyValueWhenFormFieldIsColl ); } + public function testProcessWithInvalidCollectionRenamedPropertyValueWhenFormFieldIsCollectionType() + { + $form = $this->createFormBuilder()->create('testForm', null, ['compound' => true]) + ->add( + 'renamedField1', + 'collection', + [ + 'property_path' => '[field1]', + 'type' => 'text', + 'options' => ['constraints' => [new Constraints\NotBlank()]], + 'allow_add' => true + ] + ) + ->getForm(); + $form->submit( + [ + 'renamedField1' => [1, null], + ] + ); + + $this->context->setForm($form); + $this->processor->process($this->context); + + $this->assertFalse($form->isValid()); + $this->assertTrue($this->context->hasErrors()); + $this->assertEquals( + [ + $this->createErrorObject('not blank constraint', 'This value should not be blank.', 'renamedField1.1') + ], + $this->context->getErrors() + ); + } + public function testProcessWithInvalidValueOfThirdNestedLevelProperty() { $form = $this->createFormBuilder()->create('testForm', null, ['compound' => true]) diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/InitializeApiFormExtensionTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/InitializeApiFormExtensionTest.php index 5706cd9d209..6bdc1237e50 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/InitializeApiFormExtensionTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/InitializeApiFormExtensionTest.php @@ -39,6 +39,9 @@ public function testProcess() $this->metadataTypeGuesser->expects($this->once()) ->method('setMetadataAccessor') ->with($this->isInstanceOf('Oro\Bundle\ApiBundle\Processor\ContextMetadataAccessor')); + $this->metadataTypeGuesser->expects($this->once()) + ->method('setConfigAccessor') + ->with($this->isInstanceOf('Oro\Bundle\ApiBundle\Processor\ContextConfigAccessor')); $this->processor->process($this->context); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/ProtectQueryByAclTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/ProtectQueryByAclTest.php index 9039860eadf..fe0772aa15a 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/ProtectQueryByAclTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/ProtectQueryByAclTest.php @@ -2,8 +2,7 @@ namespace Oro\Bundle\ApiBundle\Tests\Unit\Processor\Shared; -use Doctrine\Common\Collections\Criteria; - +use Oro\Bundle\ApiBundle\Collection\Criteria; use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; use Oro\Bundle\ApiBundle\Processor\Context; use Oro\Bundle\ApiBundle\Processor\Shared\ProtectQueryByAcl; @@ -51,6 +50,18 @@ public function setUp() $this->context = new Context($configProvider, $metadataProvider); } + /** + * @return Criteria + */ + protected function getCriteria() + { + $resolver = $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\EntityClassResolver') + ->disableOriginalConstructor() + ->getMock(); + + return new Criteria($resolver); + } + public function testProcessWhenQueryIsAlreadyBuilt() { $className = 'Oro\Bundle\ApiBundle\Tests\Unit\Fixtures\Entity\Product'; @@ -77,7 +88,7 @@ public function testProcessWithoutConfig() $this->context->setClassName($className); $config = new EntityDefinitionConfig(); $this->context->setConfig($config); - $criteria = new Criteria(); + $criteria = $this->getCriteria(); $this->context->setCriteria($criteria); $this->aclHelper->expects($this->once()) @@ -95,7 +106,7 @@ public function testProcessWithConfig() $aclResource = 'acme_test_delete_resource'; $config->setAclResource($aclResource); $this->context->setConfig($config); - $criteria = new Criteria(); + $criteria = $this->getCriteria(); $this->context->setCriteria($criteria); $aclAnnotation = new Acl( [ diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterConfiguredFiltersTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterConfiguredFiltersTest.php new file mode 100644 index 00000000000..034e3c36d27 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterConfiguredFiltersTest.php @@ -0,0 +1,309 @@ +context->setAction('get_list'); + + $this->filterFactory = $this->getMock('Oro\Bundle\ApiBundle\Filter\FilterFactoryInterface'); + + $this->processor = new RegisterConfiguredFilters( + $this->filterFactory, + $this->doctrineHelper + ); + } + + /** + * @param string $dataType + * + * @return ComparisonFilter + */ + protected function getComparisonFilter($dataType) + { + $filter = new ComparisonFilter($dataType); + $filter->setSupportedOperators(['=', '!=']); + + return $filter; + } + + public function testProcessWithEmptyFiltersConfig() + { + $filtersConfig = new FiltersConfig(); + + $this->filterFactory->expects($this->never()) + ->method('createFilter'); + + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + } + + /** + * @expectedException \Oro\Bundle\ApiBundle\Exception\RuntimeException + * @expectedExceptionMessage Expected "all" exclusion policy for filters. Got: none. + */ + public function testProcessWithNotNormalizedFiltersConfig() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeNone(); + + $this->filterFactory->expects($this->never()) + ->method('createFilter'); + + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + } + + public function testProcessForComparisonFilterForNotManageableEntity() + { + $className = 'Test\Class'; + $this->notManageableClassNames = [$className]; + + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDataType('string'); + $filtersConfig->addField('someField', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName($className); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('someField'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('someField', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForComparisonFilterForManageableEntity() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDataType('string'); + $filtersConfig->addField('someField', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\Category::class); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('someField'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('someField', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForFilterWithOptions() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDescription('filter description'); + $filterConfig->setType('someFilter'); + $filterConfig->setOptions(['some_option' => 'val']); + $filterConfig->setDataType('integer'); + $filterConfig->setPropertyPath('someField'); + $filterConfig->setArrayAllowed(); + $filterConfig->setOperators(['=', '<', '>']); + $filtersConfig->addField('filter', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('someFilter', ['some_option' => 'val', 'data_type' => 'integer']) + ->willReturnCallback( + function ($filterType, array $options) { + return $this->getComparisonFilter($options['data_type']); + } + ); + + $this->context->setClassName(Entity\Category::class); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setDescription('filter description'); + $expectedFilter->setDataType('integer'); + $expectedFilter->setField('someField'); + $expectedFilter->setArrayAllowed(true); + $expectedFilter->setSupportedOperators(['=', '<', '>']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForComparisonFilterForToOneAssociation() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDataType('string'); + $filterConfig->setPropertyPath('category'); + $filtersConfig->addField('filter', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('category'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForComparisonFilterForToOneAssociationField() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDataType('string'); + $filterConfig->setPropertyPath('category.name'); + $filtersConfig->addField('filter', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('category.name'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForComparisonFilterForToManyAssociation() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDataType('string'); + $filterConfig->setPropertyPath('groups'); + $filtersConfig->addField('filter', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('groups'); + $expectedFilter->setSupportedOperators(['=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForComparisonFilterForToManyAssociationField() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDataType('string'); + $filterConfig->setPropertyPath('groups.name'); + $filtersConfig->addField('filter', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('groups.name'); + $expectedFilter->setSupportedOperators(['=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForSortFilter() + { + $filtersConfig = new FiltersConfig(); + $filtersConfig->setExcludeAll(); + + $filterConfig = new FilterFieldConfig(); + $filterConfig->setDataType('string'); + $filtersConfig->addField('sort', $filterConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn(new SortFilter('string')); + + $this->context->setClassName(Entity\Category::class); + $this->context->setConfigOfFilters($filtersConfig); + $this->processor->process($this->context); + + $expectedFilter = new SortFilter('string'); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('sort', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterDynamicFiltersTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterDynamicFiltersTest.php new file mode 100644 index 00000000000..14f61bd06ac --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterDynamicFiltersTest.php @@ -0,0 +1,460 @@ +context->setAction('get_list'); + + $this->filterFactory = $this->getMock('Oro\Bundle\ApiBundle\Filter\FilterFactoryInterface'); + + $this->processor = new RegisterDynamicFilters( + $this->filterFactory, + $this->doctrineHelper, + $this->configProvider + ); + } + + /** + * @param string $dataType + * + * @return ComparisonFilter + */ + protected function getComparisonFilter($dataType) + { + $filter = new ComparisonFilter($dataType); + $filter->setSupportedOperators(['=', '!=']); + + return $filter; + } + + /** + * @param string[] $fields + * @param array $filterFields + * + * @return Config + */ + protected function getConfig(array $fields = [], array $filterFields = []) + { + $config = new Config(); + $config->setDefinition($this->getEntityDefinitionConfig($fields)); + $config->setFilters($this->getFiltersConfig($filterFields)); + + return $config; + } + + /** + * @param string[] $fields + * + * @return EntityDefinitionConfig + */ + protected function getEntityDefinitionConfig(array $fields = []) + { + $config = new EntityDefinitionConfig(); + foreach ($fields as $field) { + $config->addField($field); + } + + return $config; + } + + /** + * @param array $filterFields + * + * @return FiltersConfig + */ + protected function getFiltersConfig(array $filterFields = []) + { + $config = new FiltersConfig(); + $config->setExcludeAll(); + foreach ($filterFields as $field => $dataType) { + $config->addField($field)->setDataType($dataType); + } + + return $config; + } + + /** + * @param $queryString + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + protected function getRequest($queryString) + { + $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + $request->expects($this->once()) + ->method('getQueryString') + ->willReturn($queryString); + + return $request; + } + + public function testProcessForPrimaryEntityFields() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['name', 'label']); + $primaryEntityFilters = $this->getFiltersConfig(['name' => 'string']); + + $request = $this->getRequest('filter[name]=val1'); + + $this->configProvider->expects($this->never()) + ->method('getConfig'); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\Category::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('name'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter[name]', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForUnknownPrimaryEntityField() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['name', 'label']); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[label1]=test'); + + $this->configProvider->expects($this->never()) + ->method('getConfig'); + + $this->filterFactory->expects($this->never()) + ->method('createFilter'); + + $this->context->setClassName(Entity\Category::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $this->assertCount(0, $this->context->getFilters()); + $this->assertEquals( + [ + Error::createValidationError( + Constraint::FILTER, + sprintf('Filter "%s" is not supported.', 'filter[label1]') + )->setSource(ErrorSource::createByParameter('filter[label1]')) + ], + $this->context->getErrors() + ); + } + + public function testProcessForPrimaryEntityFieldWhichCannotBeUsedForFiltering() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['name', 'label']); + $primaryEntityFilters = $this->getFiltersConfig(['name' => 'unsupported']); + + $request = $this->getRequest('filter[label]=test'); + + $this->configProvider->expects($this->never()) + ->method('getConfig'); + + $this->filterFactory->expects($this->never()) + ->method('createFilter'); + + $this->context->setClassName(Entity\Category::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $this->assertCount(0, $this->context->getFilters()); + $this->assertEquals( + [ + Error::createValidationError( + Constraint::FILTER, + sprintf('Filter "%s" is not supported.', 'filter[label]') + )->setSource(ErrorSource::createByParameter('filter[label]')) + ], + $this->context->getErrors() + ); + } + + public function testProcessForRelatedEntityField() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category']); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[category.name]=test'); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->willReturn( + $this->getConfig( + ['name'], + ['name' => 'string'] + ) + ); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('category.name'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter[category.name]', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForRelatedEntityFieldWithNotEqualOperator() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category']); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[category.name]!=test'); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->willReturn( + $this->getConfig( + ['name'], + ['name' => 'string'] + ) + ); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('category.name'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter[category.name]', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForToManyRelatedEntityFieldWithNotEqualOperator() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'groups']); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[groups.name]!=test'); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->willReturn( + $this->getConfig( + ['name'], + ['name' => 'string'] + ) + ); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('groups.name'); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter[groups.name]', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForRelatedEntityFieldWhenAssociationDoesNotExist() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category']); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[category1.name]=test'); + + $this->configProvider->expects($this->never()) + ->method('getConfig'); + + $this->filterFactory->expects($this->never()) + ->method('createFilter'); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $this->assertCount(0, $this->context->getFilters()); + $this->assertEquals( + [ + Error::createValidationError( + Constraint::FILTER, + sprintf('Filter "%s" is not supported.', 'filter[category1.name]') + )->setSource(ErrorSource::createByParameter('filter[category1.name]')) + ], + $this->context->getErrors() + ); + } + + public function testProcessForRelatedEntityFieldWhenAssociationIsRenamed() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category1']); + $primaryEntityConfig->getField('category1')->setPropertyPath('category'); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[category1.name]=test'); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->willReturn( + $this->getConfig( + ['name'], + ['name' => 'string'] + ) + ); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('category.name'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter[category1.name]', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForRenamedRelatedEntityField() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category']); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[category.name1]=test'); + + $categoryConfig = $this->getConfig( + ['name1'], + ['name1' => 'string'] + ); + $categoryConfig->getDefinition()->getField('name1')->setPropertyPath('name'); + $categoryConfig->getFilters()->getField('name1')->setPropertyPath('name'); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->willReturn($categoryConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('category.name'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter[category.name1]', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } + + public function testProcessForRenamedAssociationAndRenamedRelatedEntityField() + { + $primaryEntityConfig = $this->getEntityDefinitionConfig(['id', 'category1']); + $primaryEntityConfig->getField('category1')->setPropertyPath('category'); + $primaryEntityFilters = $this->getFiltersConfig(); + + $request = $this->getRequest('filter[category1.name1]=test'); + + $categoryConfig = $this->getConfig( + ['name1'], + ['name1' => 'string'] + ); + $categoryConfig->getDefinition()->getField('name1')->setPropertyPath('name'); + $categoryConfig->getFilters()->getField('name1')->setPropertyPath('name'); + + $this->configProvider->expects($this->once()) + ->method('getConfig') + ->willReturn($categoryConfig); + + $this->filterFactory->expects($this->once()) + ->method('createFilter') + ->with('string', []) + ->willReturn($this->getComparisonFilter('string')); + + $this->context->setClassName(Entity\User::class); + $this->context->setConfig($primaryEntityConfig); + $this->context->setConfigOfFilters($primaryEntityFilters); + $this->context->setFilterValues(new RestFilterValueAccessor($request)); + $this->processor->process($this->context); + + $expectedFilter = new ComparisonFilter('string'); + $expectedFilter->setField('category.name'); + $expectedFilter->setSupportedOperators(['=', '!=']); + $expectedFilters = new FilterCollection(); + $expectedFilters->add('filter[category1.name1]', $expectedFilter); + + $this->assertEquals($expectedFilters, $this->context->getFilters()); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterFiltersTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterFiltersTest.php deleted file mode 100644 index d337fd3a9dd..00000000000 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RegisterFiltersTest.php +++ /dev/null @@ -1,98 +0,0 @@ -filterFactory = $this->getMock('Oro\Bundle\ApiBundle\Filter\FilterFactoryInterface'); - - $this->processor = new RegisterFilters($this->filterFactory); - } - - public function testProcessWithEmptyFiltersConfig() - { - $filtersConfig = new FiltersConfig(); - - $this->filterFactory->expects($this->never()) - ->method('createFilter'); - - $this->context->setConfigOfFilters($filtersConfig); - $this->processor->process($this->context); - } - - /** - * @expectedException \Oro\Bundle\ApiBundle\Exception\RuntimeException - * @expectedExceptionMessage Expected "all" exclusion policy for filters. Got: none. - */ - public function testProcessWithNotNormalizedFiltersConfig() - { - $filtersConfig = new FiltersConfig(); - $filtersConfig->setExcludeNone(); - - $this->filterFactory->expects($this->never()) - ->method('createFilter'); - - $this->context->setConfigOfFilters($filtersConfig); - $this->processor->process($this->context); - } - - public function testProcess() - { - $filtersConfig = new FiltersConfig(); - $filtersConfig->setExcludeAll(); - - $filter1Config = new FilterFieldConfig(); - $filter1Config->setDataType('integer'); - $filter1Config->setDescription('filter1 description'); - $filtersConfig->addField('filter1', $filter1Config); - - $filter2Config = new FilterFieldConfig(); - $filter2Config->setDataType('string'); - $filter2Config->setDescription('filter2 description'); - $filter2Config->setArrayAllowed(true); - $filtersConfig->addField('filter2', $filter2Config); - - $this->filterFactory->expects($this->exactly(2)) - ->method('createFilter') - ->willReturnMap( - [ - ['integer', new ComparisonFilter('integer')], - ['string', new SortFilter('string')], - ] - ); - - $this->context->setConfigOfFilters($filtersConfig); - $this->processor->process($this->context); - - $filters = $this->context->getFilters(); - $this->assertEquals(2, $filters->count()); - /** @var ComparisonFilter $filter1 */ - $filter1 = $filters->get('filter1'); - $this->assertEquals('filter1', $filter1->getField()); - $this->assertEquals($filter1Config->getDataType(), $filter1->getDataType()); - $this->assertEquals($filter1->getDescription(), $filter1->getDescription()); - $this->assertFalse($filter1->isArrayAllowed()); - /** @var SortFilter $filter2 */ - $filter2 = $filters->get('filter2'); - $this->assertEquals($filter2Config->getDataType(), $filter2->getDataType()); - $this->assertEquals($filter2->getDescription(), $filter2->getDescription()); - $this->assertTrue($filter2->isArrayAllowed()); - } -} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RestoreDefaultFormExtensionTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RestoreDefaultFormExtensionTest.php index 21849ea129a..2b0a9c4c15c 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RestoreDefaultFormExtensionTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/RestoreDefaultFormExtensionTest.php @@ -39,6 +39,9 @@ public function testProcess() $this->metadataTypeGuesser->expects($this->once()) ->method('setMetadataAccessor') ->with(null); + $this->metadataTypeGuesser->expects($this->once()) + ->method('setConfigAccessor') + ->with(null); $this->processor->process($this->context); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/ContextParentConfigAccessorTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/ContextParentConfigAccessorTest.php new file mode 100644 index 00000000000..b7f8118bb76 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/ContextParentConfigAccessorTest.php @@ -0,0 +1,50 @@ +context = $this->getMockBuilder('Oro\Bundle\ApiBundle\Processor\Subresource\SubresourceContext') + ->disableOriginalConstructor() + ->getMock(); + + $this->configAccessor = new ContextParentConfigAccessor($this->context); + } + + public function testGetConfigForContextParentClass() + { + $className = 'Test\Entity'; + $config = new EntityDefinitionConfig(); + + $this->context->expects($this->once()) + ->method('getParentClassName') + ->willReturn($className); + $this->context->expects($this->once()) + ->method('getParentConfig') + ->willReturn($config); + + $this->assertSame($config, $this->configAccessor->getConfig($className)); + } + + public function testGetConfigForNotContextParentClass() + { + $this->context->expects($this->once()) + ->method('getParentClassName') + ->willReturn('Test\Entity1'); + $this->context->expects($this->never()) + ->method('getParentConfig'); + + $this->assertNull($this->configAccessor->getConfig('Test\Entity2')); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/ContextParentMetadataAccessorTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/ContextParentMetadataAccessorTest.php new file mode 100644 index 00000000000..f942037a1f2 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/ContextParentMetadataAccessorTest.php @@ -0,0 +1,50 @@ +context = $this->getMockBuilder('Oro\Bundle\ApiBundle\Processor\Subresource\SubresourceContext') + ->disableOriginalConstructor() + ->getMock(); + + $this->metadataAccessor = new ContextParentMetadataAccessor($this->context); + } + + public function testGetMetadataForContextParentClass() + { + $className = 'Test\Entity'; + $metadata = new EntityMetadata(); + + $this->context->expects($this->once()) + ->method('getParentClassName') + ->willReturn($className); + $this->context->expects($this->once()) + ->method('getParentMetadata') + ->willReturn($metadata); + + $this->assertSame($metadata, $this->metadataAccessor->getMetadata($className)); + } + + public function testGetMetadataForNotContextParentClass() + { + $this->context->expects($this->once()) + ->method('getParentClassName') + ->willReturn('Test\Entity1'); + $this->context->expects($this->never()) + ->method('getParentMetadata'); + + $this->assertNull($this->metadataAccessor->getMetadata('Test\Entity2')); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/InitializeApiFormExtensionTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/InitializeApiFormExtensionTest.php index 157509725df..e1ff9eaffde 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/InitializeApiFormExtensionTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/InitializeApiFormExtensionTest.php @@ -38,11 +38,10 @@ public function testProcess() ->method('switchToApiFormExtension'); $this->metadataTypeGuesser->expects($this->once()) ->method('setMetadataAccessor') - ->with( - $this->isInstanceOf( - 'Oro\Bundle\ApiBundle\Processor\Subresource\ContextParentMetadataAccessor' - ) - ); + ->with($this->isInstanceOf('Oro\Bundle\ApiBundle\Processor\Subresource\ContextParentMetadataAccessor')); + $this->metadataTypeGuesser->expects($this->once()) + ->method('setConfigAccessor') + ->with($this->isInstanceOf('Oro\Bundle\ApiBundle\Processor\Subresource\ContextParentConfigAccessor')); $this->processor->process($this->context); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/RecognizeAssociationTypeTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/RecognizeAssociationTypeTest.php index c37f765bb4a..11e778a781a 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/RecognizeAssociationTypeTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/RecognizeAssociationTypeTest.php @@ -109,4 +109,32 @@ public function testProcessForKnownAssociation() $this->context->isCollection() ); } + + public function testProcessForSubresourceWithEmptyTargetClass() + { + $parentEntityClass = 'Test\ParentClass'; + $associationName = 'testAssociation'; + + $entitySubresources = new ApiResourceSubresources($parentEntityClass); + $entitySubresources->addSubresource('testAssociation'); + + $this->subresourcesProvider->expects($this->once()) + ->method('getSubresources') + ->with($parentEntityClass, $this->context->getVersion(), $this->context->getRequestType()) + ->willReturn($entitySubresources); + + $this->context->setParentClassName($parentEntityClass); + $this->context->setAssociationName($associationName); + $this->processor->process($this->context); + + $this->assertEquals( + [ + Error::createValidationError( + 'relationship constraint', + 'The target entity type cannot be recognized.' + ) + ], + $this->context->getErrors() + ); + } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/SaveParentEntityTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/SaveParentEntityTest.php index 36db287f90f..f62c0861a48 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/SaveParentEntityTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/SaveParentEntityTest.php @@ -69,7 +69,7 @@ public function testProcessForManageableParentEntity() $em->expects($this->once()) ->method('flush') - ->with($this->identicalTo($entity)); + ->with(null); $this->context->setParentEntity($entity); $this->processor->process($this->context); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Update/SaveEntityTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Update/SaveEntityTest.php index 27f1a463571..8cd74ca3a47 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Update/SaveEntityTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Update/SaveEntityTest.php @@ -69,7 +69,7 @@ public function testProcessForManageableEntity() $em->expects($this->once()) ->method('flush') - ->with($this->identicalTo($entity)); + ->with(null); $this->context->setResult($entity); $this->processor->process($this->context); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesCacheTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesCacheTest.php index 87d0d0c5b55..654763bfb3a 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesCacheTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesCacheTest.php @@ -105,13 +105,18 @@ public function testSave() ->method('save') ->with( 'accessible_1.2rest', - ['Test\Entity1', 'Test\Entity3'] + [ + 'Test\Entity1' => true, + 'Test\Entity2' => false, + 'Test\Entity3' => true + ] ); $this->resourcesCache->saveResources( '1.2', new RequestType(['rest']), - [$resource1, $resource2, $resource3] + [$resource1, $resource2, $resource3], + ['Test\Entity1', 'Test\Entity3'] ); } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesProviderTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesProviderTest.php index ca19ffa94fc..fd1e33933c6 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesProviderTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/ResourcesProviderTest.php @@ -16,7 +16,7 @@ class ResourcesProviderTest extends \PHPUnit_Framework_TestCase protected $resourcesCache; /** @var ResourcesProvider */ - protected $loader; + protected $resourcesProvider; protected function setUp() { @@ -27,7 +27,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->loader = new ResourcesProvider($this->processor, $this->resourcesCache); + $this->resourcesProvider = new ResourcesProvider($this->processor, $this->resourcesCache); } public function testGetResourcesNoCache() @@ -39,6 +39,7 @@ public function testGetResourcesNoCache() new ApiResource('Test\Entity1'), new ApiResource('Test\Entity3'), ]; + $expectedAccessibleResources = ['Test\Entity3']; $this->processor->expects($this->once()) ->method('process') @@ -49,6 +50,8 @@ function (CollectResourcesContext $context) use ($version, $requestType) { $context->getResult()->add(new ApiResource('Test\Entity1')); $context->getResult()->add(new ApiResource('Test\Entity3')); + + $context->setAccessibleResources(['Test\Entity3']); } ); $this->resourcesCache->expects($this->once()) @@ -57,11 +60,11 @@ function (CollectResourcesContext $context) use ($version, $requestType) { ->willReturn(null); $this->resourcesCache->expects($this->once()) ->method('saveResources') - ->with($version, $this->identicalTo($requestType), $expectedResources); + ->with($version, $this->identicalTo($requestType), $expectedResources, $expectedAccessibleResources); $this->assertEquals( $expectedResources, - $this->loader->getResources($version, $requestType) + $this->resourcesProvider->getResources($version, $requestType) ); } @@ -86,7 +89,252 @@ public function testGetResourcesFromCache() $this->assertEquals( $cachedResources, - $this->loader->getResources($version, $requestType) + $this->resourcesProvider->getResources($version, $requestType) + ); + } + + public function testGetAccessibleResourcesWhenCacheExists() + { + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $cachedData = [ + 'Test\Entity1' => false, + 'Test\Entity3' => true + ]; + + $this->processor->expects($this->never()) + ->method('process'); + $this->resourcesCache->expects($this->once()) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($cachedData); + $this->resourcesCache->expects($this->never()) + ->method('saveResources'); + + $this->assertEquals( + ['Test\Entity3'], + $this->resourcesProvider->getAccessibleResources($version, $requestType) + ); + } + + public function testGetAccessibleResourcesWhenCacheDoesNotExist() + { + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $cachedData = [ + 'Test\Entity1' => false, + 'Test\Entity3' => true + ]; + $expectedResources = [ + new ApiResource('Test\Entity1'), + new ApiResource('Test\Entity3'), + ]; + $expectedAccessibleResources = ['Test\Entity3']; + + $this->processor->expects($this->once()) + ->method('process') + ->willReturnCallback( + function (CollectResourcesContext $context) use ($version, $requestType) { + $this->assertEquals($version, $context->getVersion()); + $this->assertEquals($requestType, $context->getRequestType()); + + $context->getResult()->add(new ApiResource('Test\Entity1')); + $context->getResult()->add(new ApiResource('Test\Entity3')); + + $context->setAccessibleResources(['Test\Entity3']); + } + ); + $this->resourcesCache->expects($this->at(0)) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->at(1)) + ->method('getResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->at(2)) + ->method('saveResources') + ->with($version, $this->identicalTo($requestType), $expectedResources, $expectedAccessibleResources); + $this->resourcesCache->expects($this->at(3)) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($cachedData); + + $this->assertEquals( + ['Test\Entity3'], + $this->resourcesProvider->getAccessibleResources($version, $requestType) + ); + } + + public function testIsResourceAccessibleWhenCacheExists() + { + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $cachedData = [ + 'Test\Entity1' => false, + 'Test\Entity3' => true + ]; + + $this->processor->expects($this->never()) + ->method('process'); + $this->resourcesCache->expects($this->once()) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($cachedData); + $this->resourcesCache->expects($this->never()) + ->method('saveResources'); + + $this->assertFalse( + $this->resourcesProvider->isResourceAccessible('Test\Entity1', $version, $requestType) + ); + $this->assertFalse( + $this->resourcesProvider->isResourceAccessible('Test\Entity2', $version, $requestType) + ); + $this->assertTrue( + $this->resourcesProvider->isResourceAccessible('Test\Entity3', $version, $requestType) + ); + } + + public function testIsResourceAccessibleWhenCacheDoesNotExist() + { + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $cachedData = [ + 'Test\Entity1' => false, + 'Test\Entity3' => true + ]; + $expectedResources = [ + new ApiResource('Test\Entity1'), + new ApiResource('Test\Entity3'), + ]; + $expectedAccessibleResources = ['Test\Entity3']; + + $this->processor->expects($this->once()) + ->method('process') + ->willReturnCallback( + function (CollectResourcesContext $context) use ($version, $requestType) { + $this->assertEquals($version, $context->getVersion()); + $this->assertEquals($requestType, $context->getRequestType()); + + $context->getResult()->add(new ApiResource('Test\Entity1')); + $context->getResult()->add(new ApiResource('Test\Entity3')); + + $context->setAccessibleResources(['Test\Entity3']); + } + ); + $this->resourcesCache->expects($this->at(0)) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->at(1)) + ->method('getResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->at(2)) + ->method('saveResources') + ->with($version, $this->identicalTo($requestType), $expectedResources, $expectedAccessibleResources); + $this->resourcesCache->expects($this->at(3)) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($cachedData); + + $this->assertFalse( + $this->resourcesProvider->isResourceAccessible('Test\Entity1', $version, $requestType) + ); + $this->assertFalse( + $this->resourcesProvider->isResourceAccessible('Test\Entity2', $version, $requestType) + ); + $this->assertTrue( + $this->resourcesProvider->isResourceAccessible('Test\Entity3', $version, $requestType) + ); + } + + public function testIsResourceKnownWhenCacheExists() + { + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $cachedData = [ + 'Test\Entity1' => false, + 'Test\Entity3' => true + ]; + + $this->processor->expects($this->never()) + ->method('process'); + $this->resourcesCache->expects($this->once()) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($cachedData); + $this->resourcesCache->expects($this->never()) + ->method('saveResources'); + + $this->assertTrue( + $this->resourcesProvider->isResourceKnown('Test\Entity1', $version, $requestType) + ); + $this->assertFalse( + $this->resourcesProvider->isResourceKnown('Test\Entity2', $version, $requestType) + ); + $this->assertTrue( + $this->resourcesProvider->isResourceKnown('Test\Entity3', $version, $requestType) + ); + } + + public function testIsResourceKnownWhenCacheDoesNotExist() + { + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $cachedData = [ + 'Test\Entity1' => false, + 'Test\Entity3' => true + ]; + $expectedResources = [ + new ApiResource('Test\Entity1'), + new ApiResource('Test\Entity3'), + ]; + $expectedAccessibleResources = ['Test\Entity3']; + + $this->processor->expects($this->once()) + ->method('process') + ->willReturnCallback( + function (CollectResourcesContext $context) use ($version, $requestType) { + $this->assertEquals($version, $context->getVersion()); + $this->assertEquals($requestType, $context->getRequestType()); + + $context->getResult()->add(new ApiResource('Test\Entity1')); + $context->getResult()->add(new ApiResource('Test\Entity3')); + + $context->setAccessibleResources(['Test\Entity3']); + } + ); + $this->resourcesCache->expects($this->at(0)) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->at(1)) + ->method('getResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->at(2)) + ->method('saveResources') + ->with($version, $this->identicalTo($requestType), $expectedResources, $expectedAccessibleResources); + $this->resourcesCache->expects($this->at(3)) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($cachedData); + + $this->assertTrue( + $this->resourcesProvider->isResourceKnown('Test\Entity1', $version, $requestType) + ); + $this->assertFalse( + $this->resourcesProvider->isResourceKnown('Test\Entity2', $version, $requestType) + ); + $this->assertTrue( + $this->resourcesProvider->isResourceKnown('Test\Entity3', $version, $requestType) ); } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/SubresourcesProviderTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/SubresourcesProviderTest.php new file mode 100644 index 00000000000..aecf8cfd769 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Provider/SubresourcesProviderTest.php @@ -0,0 +1,192 @@ +processor = $this->getMockBuilder('Oro\Bundle\ApiBundle\Processor\CollectSubresourcesProcessor') + ->disableOriginalConstructor() + ->getMock(); + $this->resourcesProvider = $this->getMockBuilder('Oro\Bundle\ApiBundle\Provider\ResourcesProvider') + ->disableOriginalConstructor() + ->getMock(); + $this->resourcesCache = $this->getMockBuilder('Oro\Bundle\ApiBundle\Provider\ResourcesCache') + ->disableOriginalConstructor() + ->getMock(); + + $this->subresourcesProvider = new SubresourcesProvider( + $this->processor, + $this->resourcesProvider, + $this->resourcesCache + ); + } + + public function testGetSubresourcesNoCache() + { + $entityClass = 'Test\Entity'; + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $resources = [ + new ApiResource('Test\Entity1'), + new ApiResource('Test\Entity3'), + ]; + $accessibleResources = ['Test\Entity1']; + $expectedSubresources = new ApiResourceSubresources($entityClass); + $expectedSubresources->addSubresource('test'); + + $this->processor->expects($this->once()) + ->method('process') + ->willReturnCallback( + function (CollectSubresourcesContext $context) use ( + $version, + $requestType, + $resources, + $accessibleResources + ) { + $this->assertEquals($version, $context->getVersion()); + $this->assertEquals($requestType, $context->getRequestType()); + $this->assertEquals( + [ + 'Test\Entity1' => new ApiResource('Test\Entity1'), + 'Test\Entity3' => new ApiResource('Test\Entity3'), + ], + $context->getResources() + ); + $this->assertEquals($accessibleResources, $context->getAccessibleResources()); + + $subresources1 = new ApiResourceSubresources('Test\Entity'); + $subresources1->addSubresource('test'); + $context->getResult()->add($subresources1); + } + ); + $this->resourcesProvider->expects($this->once()) + ->method('getResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($resources); + $this->resourcesProvider->expects($this->once()) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($accessibleResources); + $this->resourcesCache->expects($this->once()) + ->method('getSubresources') + ->with($entityClass, $version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->once()) + ->method('saveSubresources') + ->with($version, $this->identicalTo($requestType), [$expectedSubresources]); + + $this->assertEquals( + $expectedSubresources, + $this->subresourcesProvider->getSubresources($entityClass, $version, $requestType) + ); + } + + public function testGetSubresourcesForUnknownEntity() + { + $entityClass = 'Test\Entity1'; + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $resources = [ + new ApiResource('Test\Entity1'), + new ApiResource('Test\Entity3'), + ]; + $accessibleResources = ['Test\Entity1']; + $subresources = new ApiResourceSubresources('Test\Entity2'); + $subresources->addSubresource('test'); + + $this->processor->expects($this->once()) + ->method('process') + ->willReturnCallback( + function (CollectSubresourcesContext $context) use ( + $version, + $requestType, + $resources, + $accessibleResources + ) { + $this->assertEquals($version, $context->getVersion()); + $this->assertEquals($requestType, $context->getRequestType()); + $this->assertEquals( + [ + 'Test\Entity1' => new ApiResource('Test\Entity1'), + 'Test\Entity3' => new ApiResource('Test\Entity3'), + ], + $context->getResources() + ); + $this->assertEquals($accessibleResources, $context->getAccessibleResources()); + + $subresources2 = new ApiResourceSubresources('Test\Entity2'); + $subresources2->addSubresource('test'); + $context->getResult()->add($subresources2); + } + ); + $this->resourcesProvider->expects($this->once()) + ->method('getResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($resources); + $this->resourcesProvider->expects($this->once()) + ->method('getAccessibleResources') + ->with($version, $this->identicalTo($requestType)) + ->willReturn($accessibleResources); + $this->resourcesCache->expects($this->once()) + ->method('getSubresources') + ->with($entityClass, $version, $this->identicalTo($requestType)) + ->willReturn(null); + $this->resourcesCache->expects($this->once()) + ->method('saveSubresources') + ->with($version, $this->identicalTo($requestType), [$subresources]); + + $this->assertNull( + $this->subresourcesProvider->getSubresources($entityClass, $version, $requestType) + ); + } + + public function testGetSubresourcesFromCache() + { + $entityClass = 'Test\Entity'; + $version = '1.2.3'; + $requestType = new RequestType([RequestType::REST, RequestType::JSON_API]); + + $expectedSubresources = new ApiResourceSubresources($entityClass); + $expectedSubresources->addSubresource('test'); + + $this->processor->expects($this->never()) + ->method('process'); + $this->resourcesProvider->expects($this->never()) + ->method('getResources'); + $this->resourcesProvider->expects($this->never()) + ->method('getAccessibleResources'); + $this->resourcesCache->expects($this->once()) + ->method('getSubresources') + ->with($entityClass, $version, $this->identicalTo($requestType)) + ->willReturn($expectedSubresources); + $this->resourcesCache->expects($this->never()) + ->method('saveSubresources'); + + $this->assertEquals( + $expectedSubresources, + $this->subresourcesProvider->getSubresources($entityClass, $version, $requestType) + ); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/ErrorCompleterTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/ErrorCompleterTest.php index b03e8e1a133..2ba14518dca 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/ErrorCompleterTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/ErrorCompleterTest.php @@ -16,9 +16,6 @@ class ErrorCompleterTest extends \PHPUnit_Framework_TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $exceptionTextExtractor; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $metadata; - /** @var ErrorCompleter */ protected $errorCompleter; @@ -26,27 +23,7 @@ protected function setUp() { $this->exceptionTextExtractor = $this->getMock('Oro\Bundle\ApiBundle\Request\ExceptionTextExtractorInterface'); - $this->metadata = $this->getMockBuilder('Oro\Bundle\ApiBundle\Metadata\EntityMetadata') - ->disableOriginalConstructor() - ->getMock(); - $this->errorCompleter = new ErrorCompleter($this->exceptionTextExtractor); - - $this->metadata = new EntityMetadata(); - $this->metadata->setIdentifierFieldNames(['id']); - $idField = new FieldMetadata(); - $idField->setName('id'); - $this->metadata->addField($idField); - $firstNameField = new FieldMetadata(); - $firstNameField->setName('firstName'); - $this->metadata->addField($firstNameField); - $userAssociation = new AssociationMetadata(); - $userAssociation->setName('user'); - $this->metadata->addAssociation($userAssociation); - $groupsAssociation = new AssociationMetadata(); - $groupsAssociation->setName('groups'); - $groupsAssociation->setIsCollection(true); - $this->metadata->addAssociation($groupsAssociation); } public function testCompleteErrorWithoutInnerException() @@ -197,9 +174,7 @@ public function testCompleteErrorWithPropertyPathButWithoutMetadata($property, $ $error->setSource(ErrorSource::createByPropertyPath($property)); $expectedError = new Error(); - if (array_key_exists('detail', $expectedResult)) { - $expectedError->setDetail($expectedResult['detail']); - } + $expectedError->setDetail($expectedResult['detail']); $this->errorCompleter->complete($error); $this->assertEquals($expectedError, $error); @@ -235,77 +210,243 @@ public function completeErrorWithPropertyPathButWithoutMetadataDataProvider() ]; } - /** - * @dataProvider completeErrorWithPropertyPathDataProvider - */ - public function testCompleteErrorWithPropertyPath($property, $expectedResult) + public function testCompleteErrorForIdentifier() { + $metadata = new EntityMetadata(); + $metadata->setIdentifierFieldNames(['id']); + $idField = new FieldMetadata(); + $idField->setName('id'); + $metadata->addField($idField); + $error = new Error(); $error->setDetail('test detail'); - $error->setSource(ErrorSource::createByPropertyPath($property)); + $error->setSource(ErrorSource::createByPropertyPath('id')); $expectedError = new Error(); - if (array_key_exists('detail', $expectedResult)) { - $expectedError->setDetail($expectedResult['detail']); - } - if (array_key_exists('source', $expectedResult)) { - $expectedSource = $expectedResult['source']; - $expectedError->setSource(new ErrorSource()); - if (array_key_exists('pointer', $expectedSource)) { - $expectedError->getSource()->setPointer($expectedSource['pointer']); - } elseif (array_key_exists('property', $expectedSource)) { - $expectedError->getSource()->setPropertyPath($expectedSource['property']); - } - } - - $this->errorCompleter->complete($error, $this->metadata); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/id')); + + $this->errorCompleter->complete($error, $metadata); $this->assertEquals($expectedError, $error); } - public function completeErrorWithPropertyPathDataProvider() + public function testCompleteErrorForField() { - return [ - [ - 'id', - [ - 'detail' => 'test detail', - 'source' => ['pointer' => '/data/id'] - ] - ], - [ - 'firstName', - [ - 'detail' => 'test detail', - 'source' => ['pointer' => '/data/attributes/firstName'] - ] - ], - [ - 'user', - [ - 'detail' => 'test detail', - 'source' => ['pointer' => '/data/relationships/user/data'] - ] - ], - [ - 'groups', - [ - 'detail' => 'test detail', - 'source' => ['pointer' => '/data/relationships/groups/data'] - ] - ], - [ - 'groups.2', - [ - 'detail' => 'test detail', - 'source' => ['pointer' => '/data/relationships/groups/data/2'] - ] - ], - [ - 'nonMappedPointer', - [ - 'detail' => 'test detail. Source: nonMappedPointer.' - ] - ] - ]; + $metadata = new EntityMetadata(); + $firstNameField = new FieldMetadata(); + $firstNameField->setName('firstName'); + $metadata->addField($firstNameField); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('firstName')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/attributes/firstName')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForToOneAssociation() + { + $metadata = new EntityMetadata(); + $userAssociation = new AssociationMetadata(); + $userAssociation->setName('user'); + $metadata->addAssociation($userAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('user')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/relationships/user/data')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForToManyAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/relationships/groups/data')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForChildOfToManyAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups.2')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/relationships/groups/data/2')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForNotMappedPointer() + { + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('notMappedPointer')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail. Source: notMappedPointer.'); + + $this->errorCompleter->complete($error, new EntityMetadata()); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForCollapsedArrayAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $groupsAssociation->setDataType('array'); + $groupsAssociation->setCollapsed(); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/attributes/groups')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForChildOfCollapsedArrayAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $groupsAssociation->setDataType('array'); + $groupsAssociation->setCollapsed(); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups.1')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/attributes/groups/1')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForChildFieldOfCollapsedArrayAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $groupsAssociation->setDataType('array'); + $groupsAssociation->setCollapsed(); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups.1.name')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/attributes/groups/1')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForNotCollapsedArrayAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $groupsAssociation->setDataType('array'); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/attributes/groups')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForChildOfNotCollapsedArrayAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $groupsAssociation->setDataType('array'); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups.1')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/attributes/groups/1')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); + } + + public function testCompleteErrorForChildFieldOfNotCollapsedArrayAssociation() + { + $metadata = new EntityMetadata(); + $groupsAssociation = new AssociationMetadata(); + $groupsAssociation->setName('groups'); + $groupsAssociation->setIsCollection(true); + $groupsAssociation->setDataType('array'); + $metadata->addAssociation($groupsAssociation); + + $error = new Error(); + $error->setDetail('test detail'); + $error->setSource(ErrorSource::createByPropertyPath('groups.1.name')); + + $expectedError = new Error(); + $expectedError->setDetail('test detail'); + $expectedError->setSource(ErrorSource::createByPointer('/data/attributes/groups/1/name')); + + $this->errorCompleter->complete($error, $metadata); + $this->assertEquals($expectedError, $error); } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/JsonApiDocumentBuilderTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/JsonApiDocumentBuilderTest.php index 3f52f52d5e4..48b3ba7d18c 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/JsonApiDocumentBuilderTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/JsonApi/JsonApiDocumentBuilderTest.php @@ -480,35 +480,14 @@ public function testAssociationWithInheritanceAndSomeInheritedEntitiesDoNotHaveA ); } - public function testAssociationsAsArrayAttributes() + public function testMissingAssociationsAsFields() { $object = [ - 'id' => 123, - 'category' => 456, - 'group' => null, - 'role' => ['id' => 789], - 'categories' => [ - ['id' => 456], - ['id' => 457] - ], - 'groups' => null, - 'products' => [], - 'roles' => [ - ['id' => 789, 'name' => 'Role1'], - ['id' => 780, 'name' => 'Role2'] - ], + 'id' => 123, ]; $metadata = $this->getEntityMetadata('Test\Entity', ['id']); $metadata->addField($this->createFieldMetadata('id')); - $metadata->addAssociation($this->createAssociationMetadata('category', 'Test\Category')); - $metadata->addAssociation($this->createAssociationMetadata('group', 'Test\Groups')); - $metadata->addAssociation($this->createAssociationMetadata('role', 'Test\Role')); - $metadata->addAssociation($this->createAssociationMetadata('categories', 'Test\Category', true)); - $metadata->addAssociation($this->createAssociationMetadata('groups', 'Test\Group', true)); - $metadata->addAssociation($this->createAssociationMetadata('products', 'Test\Product', true)); - $metadata->addAssociation($this->createAssociationMetadata('roles', 'Test\Role', true)); - $metadata->getAssociation('roles')->getTargetMetadata()->addField($this->createFieldMetadata('name')); $metadata->addAssociation($this->createAssociationMetadata('missingToOne', 'Test\Class')); $metadata->addAssociation($this->createAssociationMetadata('missingToMany', 'Test\Class', true)); foreach ($metadata->getAssociations() as $association) { @@ -522,16 +501,6 @@ public function testAssociationsAsArrayAttributes() 'type' => 'test_entity', 'id' => '123', 'attributes' => [ - 'category' => 456, - 'group' => null, - 'role' => 789, - 'categories' => [456, 457], - 'groups' => [], - 'products' => [], - 'roles' => [ - ['id' => 789, 'name' => 'Role1'], - ['id' => 780, 'name' => 'Role2'] - ], 'missingToOne' => null, 'missingToMany' => [] ] @@ -541,6 +510,313 @@ public function testAssociationsAsArrayAttributes() ); } + /** + * @dataProvider toOneAssociationAsFieldProvider + */ + public function testToOneAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'category' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('category', 'Test\Category') + ); + $association->setDataType('scalar'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'data' => [ + 'type' => 'test_entity', + 'id' => '123', + 'attributes' => [ + 'category' => $expected + ] + ], + ], + $this->documentBuilder->getDocument() + ); + } + + public function toOneAssociationAsFieldProvider() + { + return [ + [null, null], + [123, 123], + [ + ['id' => 123], + ['id' => 123, 'name' => null], + ], + [ + ['id' => 123, 'name' => 'name1'], + ['id' => 123, 'name' => 'name1'], + ], + [ + ['id' => 123, 'name' => 'name1', 'other' => 'val1'], + ['id' => 123, 'name' => 'name1'], + ], + ]; + } + + /** + * @dataProvider toManyAssociationAsFieldProvider + */ + public function testToManyAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'categories' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('categories', 'Test\Category', true) + ); + $association->setDataType('array'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'data' => [ + 'type' => 'test_entity', + 'id' => '123', + 'attributes' => [ + 'categories' => $expected + ] + ], + ], + $this->documentBuilder->getDocument() + ); + } + + public function toManyAssociationAsFieldProvider() + { + return [ + [null, []], + [[], []], + [[123, 124], [123, 124]], + [ + [['id' => 123], ['id' => 124]], + [['id' => 123, 'name' => null], ['id' => 124, 'name' => null]], + ], + [ + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + ], + [ + [ + ['id' => 123, 'name' => 'name1', 'other' => 'val1'], + ['id' => 124, 'name' => 'name2', 'other' => 'val1'] + ], + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + ], + ]; + } + + /** + * @dataProvider toOneAssociationAsFieldForIdFieldsOnlyProvider + */ + public function testToOneAssociationAsFieldForIdFieldsOnly($value, $expected) + { + $object = [ + 'id' => 123, + 'category' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('category', 'Test\Category') + ); + $association->setDataType('scalar'); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'data' => [ + 'type' => 'test_entity', + 'id' => '123', + 'attributes' => [ + 'category' => $expected + ] + ], + ], + $this->documentBuilder->getDocument() + ); + } + + public function toOneAssociationAsFieldForIdFieldsOnlyProvider() + { + return [ + [null, null], + [123, 123], + [['id' => 123], 123], + [['id' => 123, 'name' => 'name1'], 123], + ]; + } + + /** + * @dataProvider toManyAssociationAsFieldForIdFieldsOnlyProvider + */ + public function testToManyAssociationAsFieldForIdFieldsOnly($value, $expected) + { + $object = [ + 'id' => 123, + 'categories' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('categories', 'Test\Category', true) + ); + $association->setDataType('array'); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'data' => [ + 'type' => 'test_entity', + 'id' => '123', + 'attributes' => [ + 'categories' => $expected + ] + ], + ], + $this->documentBuilder->getDocument() + ); + } + + public function toManyAssociationAsFieldForIdFieldsOnlyProvider() + { + return [ + [null, []], + [[], []], + [[123, 124], [123, 124]], + [ + [['id' => 123], ['id' => 124]], + [123, 124] + ], + [ + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + [123, 124] + ], + ]; + } + + /** + * @dataProvider toOneCollapsedAssociationAsFieldProvider + */ + public function testToOneCollapsedAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'category' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('category', 'Test\Category') + ); + $association->setDataType('scalar'); + $association->setCollapsed('scalar'); + $association->getTargetMetadata()->removeField('id'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'data' => [ + 'type' => 'test_entity', + 'id' => '123', + 'attributes' => [ + 'category' => $expected + ] + ], + ], + $this->documentBuilder->getDocument() + ); + } + + public function toOneCollapsedAssociationAsFieldProvider() + { + return [ + [null, null], + ['name1', 'name1'], + [ + ['name' => 'name1'], + 'name1', + ], + [ + ['name' => 'name1', 'other' => 'val1'], + 'name1', + ], + ]; + } + + /** + * @dataProvider toManyCollapsedAssociationAsFieldProvider + */ + public function testToManyCollapsedAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'categories' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('categories', 'Test\Category', true) + ); + $association->setDataType('array'); + $association->setCollapsed('scalar'); + $association->getTargetMetadata()->removeField('id'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'data' => [ + 'type' => 'test_entity', + 'id' => '123', + 'attributes' => [ + 'categories' => $expected + ] + ], + ], + $this->documentBuilder->getDocument() + ); + } + + public function toManyCollapsedAssociationAsFieldProvider() + { + return [ + [null, []], + [[], []], + [['name1', 'name2'], ['name1', 'name2']], + [ + [['name' => 'name1'], ['name' => 'name2']], + ['name1', 'name2'], + ], + [ + [ + ['name' => 'name1', 'other' => 'val1'], + ['name' => 'name2', 'other' => 'val1'] + ], + ['name1', 'name2'], + ], + ]; + } + public function testNestedAssociationAsArrayAttribute() { $object = [ diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/Rest/RestDocumentBuilderTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/Rest/RestDocumentBuilderTest.php index 8f4a1892415..539ffb50ce7 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/Rest/RestDocumentBuilderTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/Rest/RestDocumentBuilderTest.php @@ -313,35 +313,14 @@ public function testAssociationWithInheritanceAndSomeInheritedEntitiesDoNotHaveA ); } - public function testAssociationsAsArrayAttributes() + public function testMissingAssociationsAsFields() { $object = [ - 'id' => 123, - 'category' => 456, - 'group' => null, - 'role' => ['id' => 789], - 'categories' => [ - ['id' => 456], - ['id' => 457] - ], - 'groups' => null, - 'products' => [], - 'roles' => [ - ['id' => 789, 'name' => 'Role1'], - ['id' => 780, 'name' => 'Role2'] - ], + 'id' => 123, ]; $metadata = $this->getEntityMetadata('Test\Entity', ['id']); $metadata->addField($this->createFieldMetadata('id')); - $metadata->addAssociation($this->createAssociationMetadata('category', 'Test\Category')); - $metadata->addAssociation($this->createAssociationMetadata('group', 'Test\Groups')); - $metadata->addAssociation($this->createAssociationMetadata('role', 'Test\Role')); - $metadata->addAssociation($this->createAssociationMetadata('categories', 'Test\Category', true)); - $metadata->addAssociation($this->createAssociationMetadata('groups', 'Test\Group', true)); - $metadata->addAssociation($this->createAssociationMetadata('products', 'Test\Product', true)); - $metadata->addAssociation($this->createAssociationMetadata('roles', 'Test\Role', true)); - $metadata->getAssociation('roles')->getTargetMetadata()->addField($this->createFieldMetadata('name')); $metadata->addAssociation($this->createAssociationMetadata('missingToOne', 'Test\Class')); $metadata->addAssociation($this->createAssociationMetadata('missingToMany', 'Test\Class', true)); foreach ($metadata->getAssociations() as $association) { @@ -352,16 +331,6 @@ public function testAssociationsAsArrayAttributes() $this->assertEquals( [ 'id' => 123, - 'category' => 456, - 'group' => null, - 'role' => 789, - 'categories' => [456, 457], - 'groups' => [], - 'products' => [], - 'roles' => [ - ['id' => 789, 'name' => 'Role1'], - ['id' => 780, 'name' => 'Role2'] - ], 'missingToOne' => null, 'missingToMany' => [] ], @@ -369,6 +338,283 @@ public function testAssociationsAsArrayAttributes() ); } + /** + * @dataProvider toOneAssociationAsFieldProvider + */ + public function testToOneAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'category' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('category', 'Test\Category') + ); + $association->setDataType('scalar'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'id' => 123, + 'category' => $expected + ], + $this->documentBuilder->getDocument() + ); + } + + public function toOneAssociationAsFieldProvider() + { + return [ + [null, null], + [123, 123], + [ + ['id' => 123], + ['id' => 123, 'name' => null], + ], + [ + ['id' => 123, 'name' => 'name1'], + ['id' => 123, 'name' => 'name1'], + ], + [ + ['id' => 123, 'name' => 'name1', 'other' => 'val1'], + ['id' => 123, 'name' => 'name1'], + ], + ]; + } + + /** + * @dataProvider toManyAssociationAsFieldProvider + */ + public function testToManyAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'categories' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('categories', 'Test\Category', true) + ); + $association->setDataType('array'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'id' => 123, + 'categories' => $expected + ], + $this->documentBuilder->getDocument() + ); + } + + public function toManyAssociationAsFieldProvider() + { + return [ + [null, []], + [[], []], + [[123, 124], [123, 124]], + [ + [['id' => 123], ['id' => 124]], + [['id' => 123, 'name' => null], ['id' => 124, 'name' => null]], + ], + [ + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + ], + [ + [ + ['id' => 123, 'name' => 'name1', 'other' => 'val1'], + ['id' => 124, 'name' => 'name2', 'other' => 'val1'] + ], + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + ], + ]; + } + + /** + * @dataProvider toOneAssociationAsFieldForIdFieldsOnlyProvider + */ + public function testToOneAssociationAsFieldForIdFieldsOnly($value, $expected) + { + $object = [ + 'id' => 123, + 'category' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('category', 'Test\Category') + ); + $association->setDataType('scalar'); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'id' => 123, + 'category' => $expected + ], + $this->documentBuilder->getDocument() + ); + } + + public function toOneAssociationAsFieldForIdFieldsOnlyProvider() + { + return [ + [null, null], + [123, 123], + [['id' => 123], 123], + [['id' => 123, 'name' => 'name1'], 123], + ]; + } + + /** + * @dataProvider toManyAssociationAsFieldForIdFieldsOnlyProvider + */ + public function testToManyAssociationAsFieldForIdFieldsOnly($value, $expected) + { + $object = [ + 'id' => 123, + 'categories' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('categories', 'Test\Category', true) + ); + $association->setDataType('array'); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'id' => 123, + 'categories' => $expected + ], + $this->documentBuilder->getDocument() + ); + } + + public function toManyAssociationAsFieldForIdFieldsOnlyProvider() + { + return [ + [null, []], + [[], []], + [[123, 124], [123, 124]], + [ + [['id' => 123], ['id' => 124]], + [123, 124] + ], + [ + [['id' => 123, 'name' => 'name1'], ['id' => 124, 'name' => 'name2']], + [123, 124] + ], + ]; + } + + /** + * @dataProvider toOneCollapsedAssociationAsFieldProvider + */ + public function testToOneCollapsedAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'category' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('category', 'Test\Category') + ); + $association->setDataType('scalar'); + $association->setCollapsed('scalar'); + $association->getTargetMetadata()->removeField('id'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'id' => 123, + 'category' => $expected + ], + $this->documentBuilder->getDocument() + ); + } + + public function toOneCollapsedAssociationAsFieldProvider() + { + return [ + [null, null], + ['name1', 'name1'], + [ + ['name' => 'name1'], + 'name1', + ], + [ + ['name' => 'name1', 'other' => 'val1'], + 'name1', + ], + ]; + } + + /** + * @dataProvider toManyCollapsedAssociationAsFieldProvider + */ + public function testToManyCollapsedAssociationAsField($value, $expected) + { + $object = [ + 'id' => 123, + 'categories' => $value, + ]; + + $metadata = $this->getEntityMetadata('Test\Entity', ['id']); + $metadata->addField($this->createFieldMetadata('id')); + $association = $metadata->addAssociation( + $this->createAssociationMetadata('categories', 'Test\Category', true) + ); + $association->setDataType('array'); + $association->setCollapsed('scalar'); + $association->getTargetMetadata()->removeField('id'); + $association->getTargetMetadata()->addField($this->createFieldMetadata('name')); + + $this->documentBuilder->setDataObject($object, $metadata); + $this->assertEquals( + [ + 'id' => 123, + 'categories' => $expected + ], + $this->documentBuilder->getDocument() + ); + } + + public function toManyCollapsedAssociationAsFieldProvider() + { + return [ + [null, []], + [[], []], + [['name1', 'name2'], ['name1', 'name2']], + [ + [['name' => 'name1'], ['name' => 'name2']], + ['name1', 'name2'], + ], + [ + [ + ['name' => 'name1', 'other' => 'val1'], + ['name' => 'name2', 'other' => 'val1'] + ], + ['name1', 'name2'], + ], + ]; + } + public function testNestedAssociationAsArrayAttribute() { $object = [ diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Util/ConfigNormalizerTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Util/ConfigNormalizerTest.php index 57dc15ff434..1e17d78ffe1 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Util/ConfigNormalizerTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Util/ConfigNormalizerTest.php @@ -24,7 +24,50 @@ public function testNormalizeConfig($config, $expectedConfig) public function normalizeConfigProvider() { return [ - 'field depends on another field' => [ + 'ignored fields' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field1' => [ + 'property_path' => '_' + ], + 'field2' => [ + 'property_path' => 'realField2' + ], + 'association1' => [ + 'fields' => [ + 'association11' => [ + 'fields' => [ + 'field111' => [ + 'property_path' => '_' + ], + 'field112' => null + ] + ], + ] + ], + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'property_path' => 'realField2' + ], + 'realField2' => null, + 'association1' => [ + 'fields' => [ + 'association11' => [ + 'fields' => [ + 'field112' => null + ] + ], + ] + ], + ] + ] + ], + 'field depends on another field' => [ 'config' => [ 'exclusion_policy' => 'all', 'fields' => [ @@ -44,7 +87,7 @@ public function normalizeConfigProvider() ] ] ], - 'field depends on excluded field' => [ + 'field depends on excluded field' => [ 'config' => [ 'exclusion_policy' => 'all', 'fields' => [ @@ -68,7 +111,7 @@ public function normalizeConfigProvider() ] ] ], - 'excluded field depends on another excluded field' => [ + 'excluded field depends on another excluded field' => [ 'config' => [ 'exclusion_policy' => 'all', 'fields' => [ @@ -94,7 +137,7 @@ public function normalizeConfigProvider() ] ] ], - 'field depends on excluded computed field' => [ + 'field depends on excluded computed field' => [ 'config' => [ 'exclusion_policy' => 'all', 'fields' => [ @@ -126,7 +169,7 @@ public function normalizeConfigProvider() ] ] ], - 'nested field depends on another field' => [ + 'nested field depends on another field' => [ 'config' => [ 'exclusion_policy' => 'all', 'fields' => [ @@ -158,6 +201,192 @@ public function normalizeConfigProvider() ] ] ], + 'field depends on association child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on association undefined child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field12' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null, + 'field12' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on undefined association child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on association excluded child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => [ + 'exclude' => true + ] + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'fields' => [ + 'field11' => [ + 'exclude' => false + ] + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on excluded association child field' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'exclude' => true, + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'association1' => [ + 'exclude' => false, + 'fields' => [ + 'field11' => null + ] + ], + 'field2' => [ + 'depends_on' => ['association1.field11'] + ] + ] + ] + ], + 'field depends on excluded association and its child fields' => [ + 'config' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'depends_on' => ['association1.association11.field111'] + ], + 'association1' => [ + 'exclude' => true, + 'fields' => [ + 'association11' => [ + 'exclude' => true, + 'fields' => [ + 'field111' => [ + 'exclude' => true + ] + ] + ], + ] + ], + ] + ], + 'expectedConfig' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'field2' => [ + 'depends_on' => ['association1.association11.field111'] + ], + 'association1' => [ + 'exclude' => false, + 'fields' => [ + 'association11' => [ + 'exclude' => false, + 'fields' => [ + 'field111' => [ + 'exclude' => false + ] + ] + ], + ] + ], + ] + ] + ], ]; } } diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Validator/Constraints/AllValidatorTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Validator/Constraints/AllValidatorTest.php index 270732891cf..8313dcaab1c 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Validator/Constraints/AllValidatorTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Validator/Constraints/AllValidatorTest.php @@ -8,9 +8,7 @@ namespace Oro\Bundle\ApiBundle\Tests\Unit\Validator\Constraints; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\PersistentCollection; +use Doctrine\Common\Collections\AbstractLazyCollection; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; @@ -83,14 +81,10 @@ public function getValidArguments() ]; } - public function testPersistentCollectionKeepsUninitialized() + public function testShouldKeepLazyCollectionUninitialized() { - $collection = new PersistentCollection( - $this->getMock('Doctrine\ORM\EntityManagerInterface'), - new ClassMetadata('\stdClass'), - new ArrayCollection() - ); - $collection->setInitialized(false); + /** @var AbstractLazyCollection $collection */ + $collection = $this->getMockForAbstractClass(AbstractLazyCollection::class); $this->validator->validate($collection, new All(new NotBlank())); diff --git a/src/Oro/Bundle/ApiBundle/Util/ConfigNormalizer.php b/src/Oro/Bundle/ApiBundle/Util/ConfigNormalizer.php index 4b75da733fd..635049e8fba 100644 --- a/src/Oro/Bundle/ApiBundle/Util/ConfigNormalizer.php +++ b/src/Oro/Bundle/ApiBundle/Util/ConfigNormalizer.php @@ -5,6 +5,10 @@ use Oro\Component\EntitySerializer\ConfigNormalizer as BaseConfigNormalizer; use Oro\Bundle\ApiBundle\Config\EntityDefinitionFieldConfig as FieldConfig; +/** + * This class should be synchronized with the config normalizer for ObjectNormalizer. + * @see Oro\Bundle\ApiBundle\Normalizer\ConfigNormalizer + */ class ConfigNormalizer extends BaseConfigNormalizer { /** @@ -13,14 +17,21 @@ class ConfigNormalizer extends BaseConfigNormalizer public function normalizeConfig(array $config, $parentField = null) { if (!empty($config[ConfigUtil::FIELDS])) { + $toRemove = []; foreach ($config[ConfigUtil::FIELDS] as $fieldName => $field) { - if (is_array($field) - && !empty($field[FieldConfig::DEPENDS_ON]) - && !ConfigUtil::isExclude($field) - ) { + if (!is_array($field)) { + continue; + } + if ($this->isIgnoredField($field)) { + $toRemove[] = $fieldName; + } + if (!empty($field[FieldConfig::DEPENDS_ON]) && !ConfigUtil::isExclude($field)) { $this->processDependentFields($config, $field[FieldConfig::DEPENDS_ON]); } } + foreach ($toRemove as $fieldName) { + unset($config[ConfigUtil::FIELDS][$fieldName]); + } } return parent::normalizeConfig($config, $parentField); @@ -28,15 +39,32 @@ public function normalizeConfig(array $config, $parentField = null) /** * @param array $config - * @param string[] $dependsOnFieldNames + * @param string[] $dependsOn + */ + protected function processDependentFields(array &$config, array $dependsOn) + { + foreach ($dependsOn as $dependsOnPropertyPath) { + $this->processDependentField($config, ConfigUtil::explodePropertyPath($dependsOnPropertyPath)); + } + } + + /** + * @param array $config + * @param string[] $dependsOnPropertyPath */ - protected function processDependentFields(array &$config, array $dependsOnFieldNames) + protected function processDependentField(array &$config, array $dependsOnPropertyPath) { - foreach ($dependsOnFieldNames as $dependsOnFieldName) { - if (array_key_exists($dependsOnFieldName, $config[ConfigUtil::FIELDS]) - && is_array($config[ConfigUtil::FIELDS][$dependsOnFieldName]) - && ConfigUtil::isExclude($config[ConfigUtil::FIELDS][$dependsOnFieldName]) - ) { + $dependsOnFieldName = $dependsOnPropertyPath[0]; + if (!array_key_exists(ConfigUtil::FIELDS, $config)) { + $config[ConfigUtil::FIELDS] = []; + } + if (!array_key_exists($dependsOnFieldName, $config[ConfigUtil::FIELDS])) { + $config[ConfigUtil::FIELDS][$dependsOnFieldName] = count($dependsOnPropertyPath) > 1 + ? [] + : null; + } + if (is_array($config[ConfigUtil::FIELDS][$dependsOnFieldName])) { + if (ConfigUtil::isExclude($config[ConfigUtil::FIELDS][$dependsOnFieldName])) { $config[ConfigUtil::FIELDS][$dependsOnFieldName][ConfigUtil::EXCLUDE] = false; if (!empty($config[ConfigUtil::FIELDS][$dependsOnFieldName][FieldConfig::DEPENDS_ON])) { $this->processDependentFields( @@ -45,6 +73,24 @@ protected function processDependentFields(array &$config, array $dependsOnFieldN ); } } + if (count($dependsOnPropertyPath) > 1) { + $this->processDependentField( + $config[ConfigUtil::FIELDS][$dependsOnFieldName], + array_slice($dependsOnPropertyPath, 1) + ); + } } } + + /** + * @param array $config The config of a field + * + * @return bool + */ + protected function isIgnoredField(array $config) + { + return + !empty($config[ConfigUtil::PROPERTY_PATH]) + && ConfigUtil::IGNORE_PROPERTY_PATH === $config[ConfigUtil::PROPERTY_PATH]; + } } diff --git a/src/Oro/Bundle/ApiBundle/Util/ConfigUtil.php b/src/Oro/Bundle/ApiBundle/Util/ConfigUtil.php index 1914ca626f2..8cae6e5a3d5 100644 --- a/src/Oro/Bundle/ApiBundle/Util/ConfigUtil.php +++ b/src/Oro/Bundle/ApiBundle/Util/ConfigUtil.php @@ -27,6 +27,19 @@ class ConfigUtil extends BaseConfigUtil /** a flag indicates whether an entity configuration should be merged with a configuration of a parent entity */ const INHERIT = 'inherit'; + /** + * You can use this constant as a property path for computed field + * to avoid collisions with existing getters. + * Example of usage: + * 'fields' => [ + * 'primaryPhone' => ['property_path' => '_'] + * ] + * In this example a value of primaryPhone will not be loaded + * even if an entity has getPrimaryPhone method. + * Also such field will be marked as not mapped for Symfony forms. + */ + const IGNORE_PROPERTY_PATH = '_'; + /** * Gets a native PHP array representation of each object in a given array. * diff --git a/src/Oro/Bundle/ApiBundle/Validator/Constraints/All.php b/src/Oro/Bundle/ApiBundle/Validator/Constraints/All.php index d6c69c1469d..848aa041bb7 100644 --- a/src/Oro/Bundle/ApiBundle/Validator/Constraints/All.php +++ b/src/Oro/Bundle/ApiBundle/Validator/Constraints/All.php @@ -13,7 +13,7 @@ /** * When applied to an array (or Traversable object), this constraint allows you to apply * a collection of constraints to each element of the array. - * The difference with Symfony constraint is that uninitialized PersistentCollection is not validated. + * The difference with Symfony constraint is that uninitialized lazy collection is not validated. * @see Symfony\Component\Validator\Constraints\All * @see Symfony\Component\Validator\Constraints\AllValidator * diff --git a/src/Oro/Bundle/ApiBundle/Validator/Constraints/AllValidator.php b/src/Oro/Bundle/ApiBundle/Validator/Constraints/AllValidator.php index 9f956cedecb..c71c459ec03 100644 --- a/src/Oro/Bundle/ApiBundle/Validator/Constraints/AllValidator.php +++ b/src/Oro/Bundle/ApiBundle/Validator/Constraints/AllValidator.php @@ -8,7 +8,7 @@ namespace Oro\Bundle\ApiBundle\Validator\Constraints; -use Doctrine\ORM\PersistentCollection; +use Doctrine\Common\Collections\AbstractLazyCollection; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** - * The difference with Symfony constraint is that uninitialized PersistentCollection is not validated. + * The difference with Symfony constraint is that uninitialized lazy collection is not validated. * @see Symfony\Component\Validator\Constraints\All * @see Symfony\Component\Validator\Constraints\AllValidator */ @@ -39,8 +39,7 @@ public function validate($value, Constraint $constraint) throw new UnexpectedTypeException($value, 'array or Traversable'); } - if ($value instanceof PersistentCollection && !$value->isInitialized()) { - // skip uninitialized PersistentCollection + if ($value instanceof AbstractLazyCollection && !$value->isInitialized()) { return; } diff --git a/src/Oro/Bundle/AttachmentBundle/Api/Processor/ComputeFileContent.php b/src/Oro/Bundle/AttachmentBundle/Api/Processor/ComputeFileContent.php index d172d821872..1f1ee0cdf07 100644 --- a/src/Oro/Bundle/AttachmentBundle/Api/Processor/ComputeFileContent.php +++ b/src/Oro/Bundle/AttachmentBundle/Api/Processor/ComputeFileContent.php @@ -8,7 +8,7 @@ use Oro\Component\ChainProcessor\ContextInterface; use Oro\Component\ChainProcessor\ProcessorInterface; -use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedDataContext; +use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedData\CustomizeLoadedDataContext; use Oro\Bundle\AttachmentBundle\Manager\FileManager; /** diff --git a/src/Oro/Bundle/AttachmentBundle/Tests/Unit/Api/Processor/ComputeFileContentTest.php b/src/Oro/Bundle/AttachmentBundle/Tests/Unit/Api/Processor/ComputeFileContentTest.php index 9f1fd2701cd..c39072a4f3e 100644 --- a/src/Oro/Bundle/AttachmentBundle/Tests/Unit/Api/Processor/ComputeFileContentTest.php +++ b/src/Oro/Bundle/AttachmentBundle/Tests/Unit/Api/Processor/ComputeFileContentTest.php @@ -5,7 +5,7 @@ use Gaufrette\Exception\FileNotFound; use Oro\Bundle\ApiBundle\Config\EntityDefinitionConfig; -use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedDataContext; +use Oro\Bundle\ApiBundle\Processor\CustomizeLoadedData\CustomizeLoadedDataContext; use Oro\Bundle\AttachmentBundle\Api\Processor\ComputeFileContent; class ComputeFileContentTest extends \PHPUnit_Framework_TestCase diff --git a/src/Oro/Bundle/BatchBundle/Monolog/Handler/BatchLogHandler.php b/src/Oro/Bundle/BatchBundle/Monolog/Handler/BatchLogHandler.php index 71f22ae6875..df2b573343f 100644 --- a/src/Oro/Bundle/BatchBundle/Monolog/Handler/BatchLogHandler.php +++ b/src/Oro/Bundle/BatchBundle/Monolog/Handler/BatchLogHandler.php @@ -4,6 +4,8 @@ use Akeneo\Bundle\BatchBundle\Monolog\Handler\BatchLogHandler as AkeneoBatchLogHandler; +use Monolog\Logger; + /** * Write the log into a separate log file */ @@ -12,6 +14,22 @@ class BatchLogHandler extends AkeneoBatchLogHandler /** @var bool */ protected $isActive = false; + /** + * {@inheritDoc} + * + * todo: Remove after update AkeneoBatchBundle to version without call of Monolog\Handler\StreamHandler constructor + */ + public function __construct($logDir) + { + $this->logDir = $logDir; + + $this->filePermission = null; + $this->useLocking = false; + + $this->setLevel(Logger::DEBUG); + $this->bubble = true; + } + /** * @param boolean $isActive */ diff --git a/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseCart.php b/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseCart.php index 2202949134c..b370814bdfa 100644 --- a/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseCart.php +++ b/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseCart.php @@ -102,10 +102,10 @@ public function setGrandTotal($grandTotal) } /** - * @param \DateTime $createdAt + * @param \DateTime|null $createdAt * @return $this */ - public function setCreatedAt($createdAt) + public function setCreatedAt(\DateTime $createdAt = null) { $this->createdAt = $createdAt; return $this; @@ -120,10 +120,10 @@ public function getCreatedAt() } /** - * @param \DateTime $updatedAt + * @param \DateTime|null $updatedAt * @return $this */ - public function setUpdatedAt($updatedAt) + public function setUpdatedAt(\DateTime $updatedAt = null) { $this->updatedAt = $updatedAt; return $this; diff --git a/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseOrder.php b/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseOrder.php index 0346257b2be..7de18ba5a86 100644 --- a/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseOrder.php +++ b/src/Oro/Bundle/BusinessEntitiesBundle/Entity/BaseOrder.php @@ -159,6 +159,7 @@ class BaseOrder public function __construct() { $this->addresses = new ArrayCollection(); + $this->items = new ArrayCollection(); } /** @@ -274,11 +275,11 @@ public function hasAddress(AbstractAddress $address) } /** - * @param \DateTime $createdAt + * @param \DateTime|null $createdAt * * @return $this */ - public function setCreatedAt(\DateTime $createdAt) + public function setCreatedAt(\DateTime $createdAt = null) { $this->createdAt = $createdAt; @@ -294,11 +295,11 @@ public function getCreatedAt() } /** - * @param \DateTime $updatedAt + * @param \DateTime|null $updatedAt * * @return $this */ - public function setUpdatedAt(\DateTime $updatedAt) + public function setUpdatedAt(\DateTime $updatedAt = null) { $this->updatedAt = $updatedAt; @@ -553,6 +554,35 @@ public function getItems() return $this->items; } + /** + * @param BaseOrderItem $item + * + * @return $this + */ + public function addItem(BaseOrderItem $item) + { + if (!$this->items->contains($item)) { + $this->items->add($item); + $item->setOrder($this); + } + + return $this; + } + + /** + * @param BaseOrderItem $item + * + * @return $this + */ + public function removeItem(BaseOrderItem $item) + { + if ($this->items->contains($item)) { + $this->items->removeElement($item); + } + + return $this; + } + /** * Clone relations */ diff --git a/src/Oro/Bundle/CalendarBundle/Migrations/Schema/OroCalendarBundleInstaller.php b/src/Oro/Bundle/CalendarBundle/Migrations/Schema/OroCalendarBundleInstaller.php index 4925f6f0259..13917d365d4 100644 --- a/src/Oro/Bundle/CalendarBundle/Migrations/Schema/OroCalendarBundleInstaller.php +++ b/src/Oro/Bundle/CalendarBundle/Migrations/Schema/OroCalendarBundleInstaller.php @@ -30,7 +30,7 @@ public function setExtendExtension(ExtendExtension $extendExtension) */ public function getMigrationVersion() { - return 'v1_13'; + return 'v1_14'; } /** diff --git a/src/Oro/Bundle/CalendarBundle/Migrations/Schema/v1_10/AddExtendDescription.php b/src/Oro/Bundle/CalendarBundle/Migrations/Schema/v1_10/AddExtendDescription.php deleted file mode 100644 index 5bce6bdf8ca..00000000000 --- a/src/Oro/Bundle/CalendarBundle/Migrations/Schema/v1_10/AddExtendDescription.php +++ /dev/null @@ -1,35 +0,0 @@ -getTable('oro_system_calendar'); - $table->addColumn( - 'extend_description', - 'text', - [ - 'oro_options' => [ - 'extend' => ['is_extend' => true, 'owner' => ExtendScope::OWNER_CUSTOM], - 'datagrid' => ['is_visible' => DatagridScope::IS_VISIBLE_FALSE], - 'merge' => ['display' => true], - 'dataaudit' => ['auditable' => true], - 'form' => ['type' => 'oro_resizeable_rich_text'], - 'view' => ['type' => 'html'], - ] - ] - ); - } -} diff --git a/src/Oro/Bundle/CalendarBundle/Migrations/Schema/v1_14/AddExtendDescription.php b/src/Oro/Bundle/CalendarBundle/Migrations/Schema/v1_14/AddExtendDescription.php new file mode 100644 index 00000000000..e999ae04449 --- /dev/null +++ b/src/Oro/Bundle/CalendarBundle/Migrations/Schema/v1_14/AddExtendDescription.php @@ -0,0 +1,37 @@ +getTable('oro_system_calendar'); + if (!$table->hasColumn('extend_description')) { + $table->addColumn( + 'extend_description', + 'text', + [ + 'oro_options' => [ + 'extend' => ['is_extend' => true, 'owner' => ExtendScope::OWNER_CUSTOM], + 'datagrid' => ['is_visible' => DatagridScope::IS_VISIBLE_FALSE], + 'merge' => ['display' => true], + 'dataaudit' => ['auditable' => true], + 'form' => ['type' => 'oro_resizeable_rich_text'], + 'view' => ['type' => 'html'], + ] + ] + ); + } + } +} diff --git a/src/Oro/Bundle/CalendarBundle/Resources/config/validation.yml b/src/Oro/Bundle/CalendarBundle/Resources/config/validation.yml index 942893a0206..cc1138dcc5a 100644 --- a/src/Oro/Bundle/CalendarBundle/Resources/config/validation.yml +++ b/src/Oro/Bundle/CalendarBundle/Resources/config/validation.yml @@ -17,6 +17,8 @@ Oro\Bundle\CalendarBundle\Entity\CalendarEvent: - Valid: ~ recurrence: - Valid: ~ + reminders: + - Valid: ~ Oro\Bundle\CalendarBundle\Entity\CalendarProperty: properties: diff --git a/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js b/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js index a307b1cd779..3ce33ee9faf 100644 --- a/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js +++ b/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js @@ -702,6 +702,9 @@ define(function(require) { // prepare options for jQuery FullCalendar control options = { // prepare options for jQuery FullCalendar control timezone: this.options.timezone, + displayEventEnd: { + month: true + }, selectHelper: true, events: _.bind(this.loadEvents, this), select: _.bind(this.onFcSelect, this), diff --git a/src/Oro/Bundle/CommentBundle/Entity/BaseComment.php b/src/Oro/Bundle/CommentBundle/Entity/BaseComment.php index d1c957130f4..bf0e5532445 100644 --- a/src/Oro/Bundle/CommentBundle/Entity/BaseComment.php +++ b/src/Oro/Bundle/CommentBundle/Entity/BaseComment.php @@ -197,11 +197,11 @@ public function getCreatedAt() /** * Sets creation date * - * @param \DateTime $createdAt + * @param \DateTime|null $createdAt * * @return self */ - public function setCreatedAt(\DateTime $createdAt) + public function setCreatedAt(\DateTime $createdAt = null) { $this->createdAt = $createdAt; @@ -221,11 +221,11 @@ public function getUpdatedAt() /** * Sets a date update * - * @param \DateTime $updatedAt + * @param \DateTime|null $updatedAt * * @return self */ - public function setUpdatedAt(\DateTime $updatedAt) + public function setUpdatedAt(\DateTime $updatedAt = null) { $this->updatedAt = $updatedAt; diff --git a/src/Oro/Bundle/ConfigBundle/Resources/config/oro/api.yml b/src/Oro/Bundle/ConfigBundle/Resources/config/oro/api.yml index fa3e62153ea..02ca12adc5f 100644 --- a/src/Oro/Bundle/ConfigBundle/Resources/config/oro/api.yml +++ b/src/Oro/Bundle/ConfigBundle/Resources/config/oro/api.yml @@ -48,10 +48,5 @@ oro_api: updatedAt: description: Updated At data_type: datetime - actions: - get: false - get_list: false - update: false - create: false - delete: false - delete_list: false + # this entity does not have own Data API resource + actions: false diff --git a/src/Oro/Bundle/DataGridBundle/Datasource/DatasourceInterface.php b/src/Oro/Bundle/DataGridBundle/Datasource/DatasourceInterface.php index 9d4bf695243..939aa6ae372 100644 --- a/src/Oro/Bundle/DataGridBundle/Datasource/DatasourceInterface.php +++ b/src/Oro/Bundle/DataGridBundle/Datasource/DatasourceInterface.php @@ -4,10 +4,6 @@ use Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface; -/** - * Class DatasourceInterface - * @package Oro\Bundle\DataGridBundle\Datasource - */ interface DatasourceInterface { /** diff --git a/src/Oro/Bundle/DataGridBundle/Datasource/Orm/Configs/ConfigProcessorInterface.php b/src/Oro/Bundle/DataGridBundle/Datasource/Orm/Configs/ConfigProcessorInterface.php new file mode 100644 index 00000000000..9bd5b3811f2 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Datasource/Orm/Configs/ConfigProcessorInterface.php @@ -0,0 +1,26 @@ +registry = $registry; + + } + + /** + * {@inheritdoc} + */ + public function processQuery(array $config) + { + if (array_key_exists('query', $config)) { + $queryConfig = $config['query']; + $converter = new YamlConverter(); + return $converter->parse(['query' => $queryConfig], $this->registry); + + } elseif (array_key_exists('entity', $config) && array_key_exists('repository_method', $config)) { + $entity = $config['entity']; + $method = $config['repository_method']; + $repository = $this->registry->getRepository($entity); + if (method_exists($repository, $method)) { + $qb = $repository->$method(); + if ($qb instanceof QueryBuilder) { + return $qb; + } else { + throw new DatasourceException( + sprintf( + '%s::%s() must return an instance of Doctrine\ORM\QueryBuilder, %s given', + get_class($repository), + $method, + is_object($qb) ? get_class($qb) : gettype($qb) + ) + ); + } + } else { + throw new DatasourceException(sprintf('%s has no method %s', get_class($repository), $method)); + } + + } else { + throw new DatasourceException(get_class($this).' expects to be configured with query or repository method'); + } + } + + /** + * Creates QueryBuilder for count query if configs for count query exist in configs array. + * Merges + * {@inheritdoc} + */ + public function processCountQuery(array $config) + { + if (array_key_exists('count_query', $config) && is_array($config['count_query'])) { + $queryConfig = $config['count_query']; + if (array_key_exists('query', $config) && is_array($config['query'])) { + $queryConfig = $this->mergeQueryConfigs($config); + } + $converter = new YamlConverter(); + + return $converter->parse(['query' => $queryConfig], $this->registry); + } + + return null; + } + + /** + * @param array $config + * + * @return array + */ + protected function mergeQueryConfigs(array $config) + { + return array_merge($config['query'], $config['count_query']); + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php b/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php index febe345f985..fcba92e9888 100644 --- a/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php +++ b/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\DataGridBundle\Datasource\Orm; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; @@ -10,18 +9,19 @@ use Oro\Component\DoctrineUtils\ORM\QueryHintResolver; -use Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface; +use Oro\Bundle\DataGridBundle\Datasource\Orm\Configs\ConfigProcessorInterface; use Oro\Bundle\DataGridBundle\Datasource\DatasourceInterface; use Oro\Bundle\DataGridBundle\Datasource\ParameterBinderAwareInterface; use Oro\Bundle\DataGridBundle\Datasource\ParameterBinderInterface; -use Oro\Bundle\DataGridBundle\Datasource\Orm\QueryConverter\YamlConverter; use Oro\Bundle\DataGridBundle\Datasource\ResultRecord; use Oro\Bundle\DataGridBundle\Datasource\ResultRecordInterface; + use Oro\Bundle\DataGridBundle\Event\OrmResultAfter; use Oro\Bundle\DataGridBundle\Event\OrmResultBefore; use Oro\Bundle\DataGridBundle\Event\OrmResultBeforeQuery; + use Oro\Bundle\DataGridBundle\Exception\BadMethodCallException; -use Oro\Bundle\DataGridBundle\Exception\DatasourceException; +use Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface; class OrmDatasource implements DatasourceInterface, ParameterBinderAwareInterface { @@ -30,11 +30,14 @@ class OrmDatasource implements DatasourceInterface, ParameterBinderAwareInterfac /** @var QueryBuilder */ protected $qb; + /** @var QueryBuilder */ + protected $countQb; + /** @var array|null */ protected $queryHints; - /** @var ManagerRegistry */ - protected $doctrine; + /** @var ConfigProcessorInterface */ + protected $configProcessor; /** @var DatagridInterface */ protected $datagrid; @@ -49,18 +52,18 @@ class OrmDatasource implements DatasourceInterface, ParameterBinderAwareInterfac protected $queryHintResolver; /** - * @param ManagerRegistry $doctrine + * @param ConfigProcessorInterface $processor * @param EventDispatcherInterface $eventDispatcher * @param ParameterBinderInterface $parameterBinder * @param QueryHintResolver $queryHintResolver */ public function __construct( - ManagerRegistry $doctrine, + ConfigProcessorInterface $processor, EventDispatcherInterface $eventDispatcher, ParameterBinderInterface $parameterBinder, QueryHintResolver $queryHintResolver ) { - $this->doctrine = $doctrine; + $this->configProcessor = $processor; $this->eventDispatcher = $eventDispatcher; $this->parameterBinder = $parameterBinder; $this->queryHintResolver = $queryHintResolver; @@ -72,42 +75,7 @@ public function __construct( public function process(DatagridInterface $grid, array $config) { $this->datagrid = $grid; - - if (isset($config['query'])) { - $queryConfig = array_intersect_key($config, array_flip(['query'])); - $converter = new YamlConverter(); - $this->qb = $converter->parse($queryConfig, $this->doctrine); - - } elseif (isset($config['entity']) && isset($config['repository_method'])) { - $entity = $config['entity']; - $method = $config['repository_method']; - $repository = $this->doctrine->getRepository($entity); - if (method_exists($repository, $method)) { - $qb = $repository->$method(); - if ($qb instanceof QueryBuilder) { - $this->qb = $qb; - } else { - throw new DatasourceException( - sprintf( - '%s::%s() must return an instance of Doctrine\ORM\QueryBuilder, %s given', - get_class($repository), - $method, - is_object($qb) ? get_class($qb) : gettype($qb) - ) - ); - } - } else { - throw new DatasourceException(sprintf('%s has no method %s', get_class($repository), $method)); - } - - } else { - throw new DatasourceException(get_class($this).' expects to be configured with query or repository method'); - } - - if (isset($config['hints'])) { - $this->queryHints = $config['hints']; - } - + $this->processConfigs($config); $grid->setDatasource(clone $this); } @@ -142,6 +110,16 @@ public function getResults() return $event->getRecords(); } + /** + * Returns QueryBuilder for count query if it was set + * + * @return QueryBuilder|null + */ + public function getCountQb() + { + return $this->countQb; + } + /** * Returns query builder * @@ -168,6 +146,7 @@ public function setQueryBuilder(QueryBuilder $qb) /** * {@inheritdoc} + * @deprecated since 1.10.3. */ public function getParameterBinder() { @@ -188,6 +167,19 @@ public function bindParameters(array $datasourceToDatagridParameters, $append = public function __clone() { - $this->qb = clone $this->qb; + $this->qb = clone $this->qb; + $this->countQb = $this->countQb ? clone $this->countQb : null; + } + + /** + * @param array $config + */ + protected function processConfigs(array $config) + { + $this->qb = $this->configProcessor->processQuery($config); + $this->countQb = $this->configProcessor->processCountQuery($config); + if (isset($config['hints'])) { + $this->queryHints = $config['hints']; + } } } diff --git a/src/Oro/Bundle/DataGridBundle/Datasource/ParameterBinderAwareInterface.php b/src/Oro/Bundle/DataGridBundle/Datasource/ParameterBinderAwareInterface.php index 5c023a64c7d..d788697e279 100644 --- a/src/Oro/Bundle/DataGridBundle/Datasource/ParameterBinderAwareInterface.php +++ b/src/Oro/Bundle/DataGridBundle/Datasource/ParameterBinderAwareInterface.php @@ -3,13 +3,15 @@ namespace Oro\Bundle\DataGridBundle\Datasource; /** - * Datasources that supports parameter binding must implement this interface. + * Data sources that supports parameter binding must implement this interface. */ interface ParameterBinderAwareInterface { /** * Gets parameter binder. * + * @deprecated since 1.10.3. + * * @return ParameterBinderInterface */ public function getParameterBinder(); diff --git a/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php index ba3733d10ca..5400913d60c 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php @@ -98,19 +98,6 @@ public function processConfigs(DatagridConfiguration $config) foreach ($columns as $columnName => &$column) { if (!in_array($columnName, $blackList, true)) { - // Check access to edit field in Class level. - // If access not granted - skip inline editing for such field. - $dadaFieldName = $this->getColummFieldName($columnName, $column); - if (!$this->authChecker->isGranted( - 'EDIT', - new FieldVote($objectIdentity, $dadaFieldName) - ) - ) { - if (array_key_exists(Configuration::BASE_CONFIG_KEY, $column)) { - $column[Configuration::BASE_CONFIG_KEY][Configuration::CONFIG_ENABLE_KEY] = false; - } - continue; - } $newColumn = $this->guesser->getColumnOptions( $columnName, $configItems['entity_name'], @@ -118,6 +105,13 @@ public function processConfigs(DatagridConfiguration $config) $behaviour ); + // Check access to edit field in Class level. + // If access not granted - skip inline editing for such field. + if (!$this->isFieldEditable($newColumn, $objectIdentity, $columnName, $column)) { + $column = $this->disableColumnEdit($column); + continue; + } + // frontend type key must not be replaced with default value $frontendTypeKey = PropertyInterface::FRONTEND_TYPE_KEY; if (!empty($newColumn[$frontendTypeKey])) { @@ -174,4 +168,50 @@ protected function getColummFieldName($columnName, $column) return $dadaFieldName; } + + /** + * @param array $column + * + * @return bool + */ + protected function isEnabledEdit($column) + { + if (array_key_exists(Configuration::BASE_CONFIG_KEY, $column) + && isset($column[Configuration::BASE_CONFIG_KEY][Configuration::CONFIG_ENABLE_KEY])) { + return $column[Configuration::BASE_CONFIG_KEY][Configuration::CONFIG_ENABLE_KEY]; + } + + return false; + } + + /** + * @param array $column + * + * @return mixed + */ + protected function disableColumnEdit($column) + { + if (array_key_exists(Configuration::BASE_CONFIG_KEY, $column)) { + $column[Configuration::BASE_CONFIG_KEY][Configuration::CONFIG_ENABLE_KEY] = false; + } + + return $column; + } + + /** + * @param array $newColumn + * @param $objectIdentity + * @param string $columnName + * @param array $column + * + * @return bool + */ + protected function isFieldEditable($newColumn, $objectIdentity, $columnName, $column) + { + return $this->isEnabledEdit($newColumn) + && $this->authChecker->isGranted( + 'EDIT', + new FieldVote($objectIdentity, $this->getColummFieldName($columnName, $column)) + ); + } } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php b/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php index 6610aa209aa..77bd59187ce 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php @@ -16,6 +16,9 @@ class Pager extends AbstractPager implements PagerInterface /** @var QueryBuilder */ protected $qb; + /** @var QueryBuilder */ + protected $countQb; + /** @var boolean */ protected $isTotalCalculated = false; @@ -46,7 +49,7 @@ public function __construct( $this->qb = $qb; parent::__construct($maxPerPage); - $this->aclHelper = $aclHelper; + $this->aclHelper = $aclHelper; $this->countQueryBuilderOptimizer = $countQueryOptimizer; } @@ -57,7 +60,7 @@ public function __construct( */ public function setQueryBuilder(QueryBuilder $qb) { - $this->qb = $qb; + $this->qb = $qb; $this->isTotalCalculated = false; return $this; @@ -71,6 +74,15 @@ public function getQueryBuilder() return $this->qb; } + /** + * @param QueryBuilder $countQb + */ + public function setCountQb(QueryBuilder $countQb) + { + $this->countQb = $countQb; + $this->isTotalCalculated = false; + } + /** * Calculates count * @@ -78,8 +90,9 @@ public function getQueryBuilder() */ public function computeNbResult() { - $countQb = $this->countQueryBuilderOptimizer->getCountQueryBuilder($this->getQueryBuilder()); - $query = $countQb->getQuery(); + $countQb = $this->countQb ? : $this->qb; + $countQb = $this->countQueryBuilderOptimizer->getCountQueryBuilder($countQb); + $query = $countQb->getQuery(); if (!$this->skipAclCheck) { $query = $this->aclHelper->apply($query, $this->aclPermission); } @@ -88,6 +101,7 @@ public function computeNbResult() if ($this->skipCountWalker !== null) { $useWalker = !$this->skipCountWalker; } + return QueryCountCalculator::calculateCount($query, $useWalker); } @@ -149,6 +163,9 @@ public function init() if (0 == $this->getPage() || 0 == $this->getMaxPerPage() || 0 == $this->getNbResults()) { $this->setLastPage(0); + if (0 !== $this->getMaxPerPage()) { + $query->setMaxResults($this->getMaxPerPage()); + } } else { $offset = ($this->getPage() - 1) * $this->getMaxPerPage(); diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php index 8567105883a..bfe1c9e53e8 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php @@ -53,9 +53,10 @@ public function isApplicable(DatagridConfiguration $config) */ public function visitDatasource(DatagridConfiguration $config, DatasourceInterface $datasource) { - $defaultPerPage = $config->offsetGetByPath(ToolbarExtension::PAGER_DEFAULT_PER_PAGE_OPTION_PATH, 10); - if ($datasource instanceof OrmDatasource) { + if ($datasource->getCountQb()) { + $this->pager->setCountQb($datasource->getCountQb()); + } $this->pager->setQueryBuilder($datasource->getQueryBuilder()); $this->pager->setSkipAclCheck($config->isDatasourceSkipAclApply()); $this->pager->setAclPermission($config->getDatasourceAclApplyPermission()); @@ -64,15 +65,26 @@ public function visitDatasource(DatagridConfiguration $config, DatasourceInterfa ); } - if ($config->offsetGetByPath(ToolbarExtension::PAGER_ONE_PAGE_OPTION_PATH, false) || - $config->offsetGetByPath(ModeExtension::MODE_OPTION_PATH) === ModeExtension::MODE_CLIENT - ) { + $onePage = $config->offsetGetByPath(ToolbarExtension::PAGER_ONE_PAGE_OPTION_PATH, false); + $mode = $config->offsetGetByPath(ModeExtension::MODE_OPTION_PATH); + $perPageLimit = $config->offsetGetByPath(ToolbarExtension::PAGER_DEFAULT_PER_PAGE_OPTION_PATH); + $defaultPerPage = $config->offsetGetByPath(ToolbarExtension::PAGER_DEFAULT_PER_PAGE_OPTION_PATH, 10); + $perPageCount = $this->getOr(PagerInterface::PER_PAGE_PARAM, $defaultPerPage); + + if ((!$perPageLimit && $onePage) || $mode === ModeExtension::MODE_CLIENT) { // no restrictions applied $this->pager->setPage(0); $this->pager->setMaxPerPage(0); + } elseif ($onePage && $perPageLimit) { + // one page with limit + $this->pager->setPage(0); + if ($config->offsetGetByPath(ToolbarExtension::TOOLBAR_HIDE_OPTION_PATH)) { + $this->pager->adjustTotalCount($perPageCount); + } + $this->pager->setMaxPerPage($perPageCount); } else { $this->pager->setPage($this->getOr(PagerInterface::PAGE_PARAM, 1)); - $this->pager->setMaxPerPage($this->getOr(PagerInterface::PER_PAGE_PARAM, $defaultPerPage)); + $this->pager->setMaxPerPage($perPageCount); } $this->tryAdjustTotalCount(); $this->pager->init(); diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Toolbar/ToolbarExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Toolbar/ToolbarExtension.php index 71cc19423b1..5323250ade0 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Toolbar/ToolbarExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Toolbar/ToolbarExtension.php @@ -18,6 +18,7 @@ class ToolbarExtension extends AbstractExtension const OPTIONS_PATH = '[options]'; const TOOLBAR_OPTION_PATH = '[options][toolbarOptions]'; + const TOOLBAR_HIDE_OPTION_PATH = '[options][toolbarOptions][hide]'; const PAGER_ITEMS_OPTION_PATH = '[options][toolbarOptions][pageSize][items]'; const PAGER_DEFAULT_PER_PAGE_OPTION_PATH = '[options][toolbarOptions][pageSize][default_per_page]'; const PAGER_ONE_PAGE_OPTION_PATH = '[options][toolbarOptions][pagination][onePage]'; diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/data_sources.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/data_sources.yml index d3acecd207b..0117e0c9d2e 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/data_sources.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/data_sources.yml @@ -6,7 +6,7 @@ services: oro_datagrid.datasource.orm: class: %oro_datagrid.datasource.orm.class% arguments: - - '@doctrine' + - '@oro_datagrid.datasource.orm.configs.yaml_processor' - '@oro_datagrid.event.dispatcher' - '@oro_datagrid.datasource.orm.parameter_binder' - '@oro_entity.query_hint_resolver' @@ -15,3 +15,8 @@ services: oro_datagrid.datasource.orm.parameter_binder: class: %oro_datagrid.datasource.orm.parameter_binder.class% + + oro_datagrid.datasource.orm.configs.yaml_processor: + class: Oro\Bundle\DataGridBundle\Datasource\Orm\Configs\YamlProcessor + arguments: + - '@doctrine' diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less index 94873f08cd5..cb94d662c55 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less @@ -365,40 +365,21 @@ td > .nowrap-ellipsis { .grid-header-cell{ padding-right: 0; } - .grid-header-cell-link { + .grid-header-cell__link, .grid-header-cell__label-container{ width: 100%; display: flex; } - .grid-header-cell-label { + .grid-header-cell__label { text-overflow: ellipsis; display: block; overflow: hidden; float: left; } - &.floatThead .grid-header.thead-sizing .grid-header-cell:not(.abbreviated) .grid-header-cell-label { + &.floatThead .grid-header.thead-sizing .grid-header-cell:not(.abbreviated) .grid-header-cell__label { width: 0 !important; } - &:not(.floatThead) .grid-header .grid-header-cell:not(.abbreviated) .grid-header-cell-label, - .grid-header.thead-sizing .grid-header-cell:not(.abbreviated) .grid-header-cell-label { + &:not(.floatThead) .grid-header .grid-header-cell:not(.abbreviated) .grid-header-cell__label, + .grid-header.thead-sizing .grid-header-cell:not(.abbreviated) .grid-header-cell__label { width: 40px !important; } } -.grid-header-cell-hint { - position: absolute; - display: block; - background: #fff; - padding: 0 7px; - border-radius: 3px; - box-shadow: 1px 1px 3px rgba(0,0,0,0.2); - z-index: 10000; - &:after{ - content: ''; - display: inline-block; - border-left: 6px solid rgba(0, 0, 0, 0); - border-right: 6px solid rgba(0, 0, 0, 0); - border-bottom: 6px solid #fff; - position: absolute; - top: -4px; - left: 6px; - } -} diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js index 6ded23855f8..b3b16cff27d 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js @@ -1,6 +1,7 @@ define([ - './string-cell' -], function(StringCell) { + './string-cell', + 'backgrid' +], function(StringCell, Backgrid) { 'use strict'; var HtmlCell; @@ -13,12 +14,20 @@ define([ * @extends oro.datagrid.cell.StringCell */ HtmlCell = StringCell.extend({ + /** + * use a default implementation to do not affect html content + * @property {(Backgrid.CellFormatter)} + */ + formatter: new Backgrid.CellFormatter(), + /** * Render a text string in a table cell. The text is converted from the * model's raw value for this cell's column. */ render: function() { - this.$el.empty().html(this.model.get(this.column.get('name'))); + var value = this.model.get(this.column.get('name')); + var formattedValue = this.formatter.fromRaw(value); + this.$el.html(formattedValue); return this; } }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js index 9efa7aa75ca..ee7749426db 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js @@ -20,13 +20,13 @@ define([ /** @property */ template: _.template( '<% if (sortable) { %>' + - '' + - '<%- label %>' + + '' + + '<%- label %>' + '' + '' + '<% } else { %>' + - '' + - '<%- label %>' + + '' + + '<%- label %>' + '' + '<% } %>' ), @@ -179,7 +179,7 @@ define([ onMouseEnter: function(e) { var _this = this; - var $label = this.$('.grid-header-cell-label'); + var $label = this.$('.grid-header-cell__label'); // measure text content var realWidth = $label[0].clientWidth; @@ -214,7 +214,7 @@ define([ onMouseLeave: function(e) { clearTimeout(this.hintTimeout); - var $label = this.$('.grid-header-cell-label'); + var $label = this.$('.grid-header-cell__label'); $label.popover('hide'); $label.popover('destroy'); this.popoverAdded = false; diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/Orm/Configs/YamlProcessorTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/Orm/Configs/YamlProcessorTest.php new file mode 100644 index 00000000000..e2464fa41f4 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/Orm/Configs/YamlProcessorTest.php @@ -0,0 +1,188 @@ +registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') + ->disableOriginalConstructor() + ->getMock(); + $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->processor = new YamlProcessor($this->registry); + } + + public function testProcessQuery() + { + $entity1 = 'EntityTest1'; + $this->registry->expects($this->once()) + ->method('getManagerForClass') + ->with($entity1) + ->willReturn($this->em); + $qb = new QueryBuilder($this->em); + + $this->em + ->expects($this->once()) + ->method('createQueryBuilder') + ->willReturn($qb); + + $configs = [ + 'type' => 'orm', + 'query' => [ + 'select' => [ + 't1.id', + 't2.id as t2_id' + ], + 'from' => [['table' => $entity1, 'alias' => 't1']], + 'join' => [ + 'left' => [['join' => 't1.test2', 'alias' => 't2']] + ], + 'where' => [ + 'and' => ['t1.type = someType'] + ] + ] + ]; + $queryBuilder = $this->processor->processQuery($configs); + + $this->assertSame($queryBuilder, $qb); + $this->assertEquals( + 'SELECT t1.id, t2.id as t2_id FROM EntityTest1 t1 LEFT JOIN t1.test2 t2 WHERE t1.type = someType', + $queryBuilder->getDQL() + ); + } + + // @codingStandardsIgnoreStart + /** + * @expectedException \Oro\Bundle\DataGridBundle\Exception\DatasourceException + * @expectedExceptionMessage Oro\Bundle\DataGridBundle\Datasource\Orm\Configs\YamlProcessor expects to be configured with query or repository method + */ + // @codingStandardsIgnoreEnd + public function testNoQueryAndRepositoryConfigsShouldThrowException() + { + $configs = [ + 'type' => 'orm', + ]; + $this->processor->processQuery($configs); + } + + /** + * @expectedException \Oro\Bundle\DataGridBundle\Exception\DatasourceException + * @expectedExceptionMessage Doctrine\ORM\EntityRepository has no method notExistedMethod + */ + public function testEntityRepositoryDoesNotHasMethodShouldThrowException() + { + $entity1 = 'EntityTest1'; + + $configs = [ + 'type' => 'orm', + 'entity' => $entity1, + 'repository_method' => 'notExistedMethod' + ]; + $repo = new EntityRepository($this->em, new ClassMetadata($entity1)); + $this->registry->expects($this->once()) + ->method('getRepository') + ->with($entity1) + ->willReturn($repo); + $this->processor->processQuery($configs); + } + + public function testConfigMethodDoNotReturnQueryBuilderShouldThrowException() + { + $entity1 = 'EntityTest1'; + + $configs = [ + 'type' => 'orm', + 'entity' => $entity1, + 'repository_method' => 'methodNotReturnQB' + ]; + $repo = $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->setMethods(['methodNotReturnQB']) + ->disableOriginalConstructor() + ->getMock(); + $repo->expects($this->once()) + ->method('methodNotReturnQB') + ->willReturn(null); + + $this->registry->expects($this->once()) + ->method('getRepository') + ->with($entity1) + ->willReturn($repo); + + $this->setExpectedException( + 'Oro\Bundle\DataGridBundle\Exception\DatasourceException', + sprintf( + '%s::methodNotReturnQB() must return an instance of Doctrine\ORM\QueryBuilder, %s given', + get_class($repo), + gettype(null) + ) + ); + $this->processor->processQuery($configs); + } + + public function testMergeCountAndBaseQueryConfigs() + { + $entity1 = 'EntityTest1'; + $this->registry->expects($this->once()) + ->method('getManagerForClass') + ->with($entity1) + ->willReturn($this->em); + $qb = new QueryBuilder($this->em); + + $this->em + ->expects($this->once()) + ->method('createQueryBuilder') + ->willReturn($qb); + + $configs = [ + 'type' => 'orm', + 'query' => [ + 'select' => [ + 't1.id', + 't2.id as t2_id' + ], + 'from' => [['table' => $entity1, 'alias' => 't1']], + 'join' => [ + 'left' => [['join' => 't1.test2', 'alias' => 't2']] + ], + 'where' => [ + 'and' => ['t1.type = someType'] + ] + ], + 'count_query' => [ + 'select' => [ + 't1.id' + ], + 'join' => null, + ] + ]; + $queryBuilder = $this->processor->processCountQuery($configs); + + $this->assertSame($queryBuilder, $qb); + $this->assertEquals( + 'SELECT t1.id FROM EntityTest1 t1 WHERE t1.type = someType', + $queryBuilder->getDQL() + ); + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/Orm/OrmDatasourceTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/Orm/OrmDatasourceTest.php index 29288124828..337cec2ac0c 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/Orm/OrmDatasourceTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/Orm/OrmDatasourceTest.php @@ -2,6 +2,9 @@ namespace Oro\Bundle\DataGridBundle\Tests\Unit\Datasource\Orm; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Query; use Doctrine\ORM\Configuration; use Doctrine\ORM\Mapping\ClassMetadata; @@ -9,6 +12,8 @@ use Oro\Component\DoctrineUtils\ORM\QueryHintResolver; +use Oro\Bundle\DataGridBundle\Datasource\Orm\Configs\YamlProcessor; +use Oro\Bundle\DataGridBundle\Datasource\ParameterBinderInterface; use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; class OrmDatasourceTest extends \PHPUnit_Framework_TestCase @@ -16,16 +21,16 @@ class OrmDatasourceTest extends \PHPUnit_Framework_TestCase /** @var OrmDatasource */ protected $datasource; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var EntityManager|\PHPUnit_Framework_MockObject_MockObject */ protected $em; - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $doctrine; + /** @var YamlProcessor|\PHPUnit_Framework_MockObject_MockObject */ + protected $processor; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $eventDispatcher; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var ParameterBinderInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $parameterBinder; protected function setUp() @@ -34,7 +39,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->doctrine = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') + $this->processor = $this->getMockBuilder('Oro\Bundle\DataGridBundle\Datasource\Orm\Configs\YamlProcessor') ->disableOriginalConstructor() ->getMock(); @@ -42,7 +47,7 @@ protected function setUp() $this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $queryHintResolver = new QueryHintResolver(); $this->datasource = new OrmDatasource( - $this->doctrine, + $this->processor, $this->eventDispatcher, $this->parameterBinder, $queryHintResolver @@ -65,11 +70,6 @@ public function testHints($hints, $expected) $configs['hints'] = $hints; } - $this->doctrine->expects($this->once()) - ->method('getManagerForClass') - ->with('Test') - ->willReturn($this->em); - $this->prepareEntityManagerForTestHints($entityClass); $query = new Query($this->em); @@ -82,13 +82,14 @@ public function testHints($hints, $expected) $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') ->setConstructorArgs([$this->em]) ->getMock(); - $this->em->expects($this->once()) - ->method('createQueryBuilder') - ->will($this->returnValue($qb)); + $qb->expects($this->once()) ->method('getQuery') ->will($this->returnValue($query)); - + $this->processor + ->expects($this->once()) + ->method('processQuery') + ->willReturn($qb); $datagrid = $this->getMock('Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface'); $this->datasource->process($datagrid, $configs); $this->datasource->getResults(); @@ -201,22 +202,17 @@ public function testBindParametersWorks() ] ]; - $this->doctrine->expects($this->once()) - ->method('getManagerForClass') - ->with('Test') - ->willReturn($this->em); - $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') ->setConstructorArgs([$this->em]) ->getMock(); - $this->em->expects($this->once()) - ->method('createQueryBuilder') - ->will($this->returnValue($qb)); $this->parameterBinder->expects($this->once()) ->method('bindParameters') ->with($datagrid, $parameters, $append); - + $this->processor + ->expects($this->once()) + ->method('processQuery') + ->willReturn($qb); $this->datasource->process($datagrid, $configs); $this->datasource->bindParameters($parameters, $append); } diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/InlineEditing/InlineEditingExtensionTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/InlineEditing/InlineEditingExtensionTest.php index 7ed763565f1..097bebab118 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/InlineEditing/InlineEditingExtensionTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/InlineEditing/InlineEditingExtensionTest.php @@ -204,6 +204,9 @@ protected function getProcessConfigsCallBack() 'two' => 'Two', ] ]; + case 'nonAvailable1': + case 'nonAvailable2': + return [Configuration::BASE_CONFIG_KEY => ['enable' => 'true']]; } return []; diff --git a/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php b/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php index 0adde35f387..1f2bbf95cc0 100644 --- a/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php +++ b/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php @@ -129,11 +129,12 @@ protected function allowSaveAttachment($size) } if ($this->configManager) { + /** Maximum sync attachment size, Mb. */ $attachmentSyncMaxSize = $this->configManager->get(self::ORO_EMAIL_ATTACHMENT_SYNC_MAX_SIZE); } // unlimited or size < configured max size - return $attachmentSyncMaxSize === 0 || $size / 1024 / 1024 <= $attachmentSyncMaxSize; + return $attachmentSyncMaxSize === 0 || $size / 1000 / 1000 <= $attachmentSyncMaxSize; } /** diff --git a/src/Oro/Bundle/EmailBundle/Command/PurgeEmailAttachmentCommand.php b/src/Oro/Bundle/EmailBundle/Command/PurgeEmailAttachmentCommand.php new file mode 100644 index 00000000000..6429e446310 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Command/PurgeEmailAttachmentCommand.php @@ -0,0 +1,192 @@ +setName(static::NAME) + ->setDescription('Purges emails attachments') + ->addOption( + static::OPTION_SIZE, + null, + InputOption::VALUE_OPTIONAL, + 'Purges emails attachments larger that option size in MB. Default to system configuration value.' + ) + ->addOption( + static::OPTION_ALL, + null, + InputOption::VALUE_NONE, + 'Purges all emails attachments ignoring "size" option.' + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $size = $this->getSize($input); + + $emailAttachments = $this->getEmailAttachments($size); + + $count = count($emailAttachments); + if ($count) { + $em = $this->getEntityManager(); + $progress = new ProgressBar($output, $count); + $progress->setFormat('debug'); + + $progress->start(); + foreach ($emailAttachments as $attachment) { + $this->removeAttachment($em, $attachment, $size); + $progress->advance(); + } + $progress->finish(); + } else { + $output->writeln('No emails attachments to purify.'); + } + } + + /** + * Returns size in bytes + * + * @param InputInterface $input + * + * @return int + */ + protected function getSize(InputInterface $input) + { + $all = $input->getOption(static::OPTION_ALL); + $size = $input->getOption(static::OPTION_SIZE); + + if ($all) { + return 0; + } + + if ($size === null) { + $size = $this->getConfigManager()->get('oro_email.attachment_sync_max_size'); + } + + /** Convert Megabytes to Bytes */ + return (int)$size * pow(10, 6); + } + + /** + * @param EntityManager $em + * @param EmailAttachment $attachment + * @param int $size + */ + protected function removeAttachment(EntityManager $em, EmailAttachment $attachment, $size) + { + // Double check of attachment size + if ($size) { + if ($attachment->getSize() < $size) { + return; + } + } + + $em->remove($attachment); + } + + /** + * @param int $size + * + * @return BufferedQueryResultIterator + */ + protected function getEmailAttachments($size) + { + $qb = $this->createEmailAttachmentQb($size); + $em = $this->getEntityManager(); + + $emailAttachments = (new BufferedQueryResultIterator($qb)) + ->setBufferSize(static::LIMIT) + ->setPageCallback( + function () use ($em) { + $em->flush(); + $em->clear(); + } + ); + + return $emailAttachments; + } + + /** + * @param int $size + * + * @return QueryBuilder + */ + protected function createEmailAttachmentQb($size) + { + $qb = $this->getEmailAttachmentRepository() + ->createQueryBuilder('a') + ->join('a.attachmentContent', 'attachment_content'); + + if ($size > 0) { + $qb + ->andWhere( + <<<'DQL' + CASE WHEN attachment_content.contentTransferEncoding = 'base64' THEN + (LENGTH(attachment_content.content) - LENGTH(attachment_content.content)/77) * 3 / 4 - 2 +ELSE + LENGTH(attachment_content.content) +END >= :size +DQL + ) + ->setParameter('size', $size); + } + + return $qb; + } + + /** + * @return ConfigManager + */ + protected function getConfigManager() + { + return $this->getContainer()->get('oro_config.global'); + } + + /** + * @return EntityRepository + */ + protected function getEmailAttachmentRepository() + { + return $this->getContainer()->get('doctrine') + ->getRepository('OroEmailBundle:EmailAttachment'); + } + + /** + * @return EntityManager + */ + protected function getEntityManager() + { + return $this->getContainer()->get('doctrine') + ->getManagerForClass('OroEmailBundle:EmailAttachment'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Controller/EmailController.php b/src/Oro/Bundle/EmailBundle/Controller/EmailController.php index 9e9f9bf30fe..041750aa320 100644 --- a/src/Oro/Bundle/EmailBundle/Controller/EmailController.php +++ b/src/Oro/Bundle/EmailBundle/Controller/EmailController.php @@ -8,6 +8,8 @@ use FOS\RestBundle\Util\Codes; +use JMS\JobQueueBundle\Entity\Job; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -32,6 +34,7 @@ use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; use Oro\Bundle\DataGridBundle\Extension\MassAction\MassActionDispatcher; use Oro\Bundle\SecurityBundle\Annotation\AclAncestor; +use Oro\Bundle\EmailBundle\Command\PurgeEmailAttachmentCommand; use Oro\Bundle\EmailBundle\Provider\EmailRecipientsHelper; /** @@ -45,6 +48,32 @@ */ class EmailController extends Controller { + /** + * @Route("/purge-emails-attachments", name="oro_email_purge_emails_attachments") + * @AclAncestor("oro_config_system") + */ + public function purgeEmailsAttachmentsAction() + { + $job = new Job(PurgeEmailAttachmentCommand::NAME); + $em = $this->getDoctrine()->getManagerForClass(Job::class); + $em->persist($job); + $em->flush(); + + return new JsonResponse([ + 'message' => $this->get('translator')->trans( + 'oro.email.controller.job_scheduled.message', + [ + '%link%' => sprintf( + '%s', + $this->get('router')->generate('oro_cron_job_view', ['id' => $job->getId()]), + $this->get('translator')->trans('oro.email.controller.job_progress') + ) + ] + ), + 'successful' => true, + ]); + } + /** * @Route("/view/{id}", name="oro_email_view", requirements={"id"="\d+"}) * @AclAncestor("oro_email_email_view") diff --git a/src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php index 41a4e1949ad..388c244aa28 100644 --- a/src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/EmailBundle/DependencyInjection/Configuration.php @@ -52,7 +52,7 @@ public function getConfigTreeBuilder() 'minimum_input_length' => ['value' => 2], 'show_recent_emails_in_user_bar' => ['value' => true], 'attachment_sync_enable' => ['value' => true], - 'attachment_sync_max_size' => ['value' => 0], + 'attachment_sync_max_size' => ['value' => 50], 'attachment_preview_limit' => ['value' => 8], 'sanitize_html' => ['value' => false] ] diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php b/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php index f65bfe02055..99e928c73ed 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php @@ -20,7 +20,8 @@ * @ORM\Table( * name="oro_email_user", * indexes={ - * @ORM\Index(name="mailbox_seen_idx", columns={"mailbox_owner_id", "is_seen"}) + * @ORM\Index(name="seen_idx", columns={"is_seen", "mailbox_owner_id"}), + * @ORM\Index(name="received_idx", columns={"received", "is_seen", "mailbox_owner_id"}) * } * ) * @ORM\HasLifecycleCallbacks diff --git a/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php index b922d36360a..4e954f50f0d 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php +++ b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php @@ -60,24 +60,15 @@ public function findEmailByMessageId($messageId) */ public function getNewEmails(User $user, Organization $organization, $limit, $folderId) { - $qb = $this->getEntityManager()->createQueryBuilder(); - $qb->select('eu') - ->from('OroEmailBundle:EmailUser', 'eu') - ->leftJoin('eu.email', 'e') - ->where($this->getAclWhereCondition($user, $organization)) - ->orderBy('eu.seen', 'ASC') - ->addOrderBy('e.sentAt', 'DESC') - ->setParameter('organization', $organization) - ->setParameter('owner', $user) - ->setMaxResults($limit); - - if ($folderId > 0) { - $qb->leftJoin('eu.folders', 'f') - ->andWhere('f.id = :folderId') - ->setParameter('folderId', $folderId); + $qb = $this->getEmailList($user, $organization, $limit, $folderId, false); + $newEmails = $qb->getQuery()->getResult(); + if (count($newEmails) < $limit) { + $qb = $this->getEmailList($user, $organization, $limit - count($newEmails), $folderId, true); + $seenEmails = $qb->getQuery()->getResult(); + $newEmails = array_merge($newEmails, $seenEmails); } - return $qb->getQuery()->getResult(); + return $newEmails; } /** @@ -249,4 +240,37 @@ protected function getAclWhereCondition(User $user, Organization $organization) return $andExpr; } } + + /** + * @param User $user + * @param Organization $organization + * @param integer $limit + * @param integer $folderId + * @param bool $isSeen + * + * @return QueryBuilder + */ + protected function getEmailList(User $user, Organization $organization, $limit, $folderId, $isSeen) + { + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('eu') + ->from('OroEmailBundle:EmailUser', 'eu') + ->where($this->getAclWhereCondition($user, $organization)) + ->andWhere('eu.seen = :seen') + ->orderBy('eu.receivedAt', 'DESC') + ->setParameter('organization', $organization) + ->setParameter('owner', $user) + ->setParameter('seen', $isSeen) + ->setMaxResults($limit); + + if ($folderId > 0) { + $qb->leftJoin('eu.folders', 'f') + ->andWhere('f.id = :folderId') + ->setParameter('folderId', $folderId); + + return $qb; + } + + return $qb; + } } diff --git a/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php b/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php index aa8e5c5ba6e..c1c2d86aa8c 100644 --- a/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php +++ b/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php @@ -2,39 +2,30 @@ namespace Oro\Bundle\EmailBundle\EventListener\Datagrid; -use Doctrine\Bundle\DoctrineBundle\Registry; +use Doctrine\ORM\Query\Expr\GroupBy; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; -use Oro\Bundle\DataGridBundle\Datasource\ResultRecord; use Oro\Bundle\DataGridBundle\Event\BuildAfter; -use Oro\Bundle\DataGridBundle\Event\OrmResultAfter; use Oro\Bundle\DataGridBundle\Event\OrmResultBeforeQuery; + use Oro\Bundle\EmailBundle\Datagrid\EmailQueryFactory; -use Oro\Component\DoctrineUtils\ORM\QueryUtils; class EmailGridListener { - /** - * @var Registry - */ - protected $registry; - /** * @var EmailQueryFactory */ protected $factory; /** - * @var QueryBuilder|null - */ - protected $qb; - - /** - * @var string|null + * Stores join's root and alias if joins for filters are added - ['eu' => ['alias1']] + * + * @var [] */ - protected $select; + protected $filterJoins; /** * @param EmailQueryFactory $factory @@ -44,6 +35,18 @@ public function __construct(EmailQueryFactory $factory) $this->factory = $factory; } + /** + * @param OrmResultBeforeQuery $event + */ + public function onResultBeforeQuery(OrmResultBeforeQuery $event) + { + $qb = $event->getQueryBuilder(); + if ($this->filterJoins) { + $this->removeJoinByRootAndAliases($qb, $this->filterJoins); + $this->removeGroupByPart($qb, 'eu.id'); + } + } + /** * Add required filters * @@ -53,10 +56,14 @@ public function onBuildAfter(BuildAfter $event) { /** @var OrmDatasource $ormDataSource */ $ormDataSource = $event->getDatagrid()->getDatasource(); - $queryBuilder = $ormDataSource->getQueryBuilder(); - $parameters = $event->getDatagrid()->getParameters(); + $queryBuilder = $ormDataSource->getQueryBuilder(); + $countQb = $ormDataSource->getCountQb(); + $parameters = $event->getDatagrid()->getParameters(); $this->factory->applyAcl($queryBuilder); + if ($countQb) { + $this->factory->applyAcl($countQb); + } if ($parameters->has('emailIds')) { $emailIds = $parameters->get('emailIds'); @@ -66,74 +73,100 @@ public function onBuildAfter(BuildAfter $event) $queryBuilder->andWhere($queryBuilder->expr()->in('e.id', $emailIds)); } - $this->prepareQueryToFilter($parameters, $queryBuilder); + $this->prepareQueryToFilter($parameters, $queryBuilder, $countQb); } /** - * @param OrmResultBeforeQuery $event + * Add joins and group by for query just if filter used. For performance optimization - BAP-10674 + * + * @param ParameterBag $parameters + * @param QueryBuilder $queryBuilder + * @param QueryBuilder $countQb */ - public function onResultBeforeQuery(OrmResultBeforeQuery $event) + protected function prepareQueryToFilter($parameters, QueryBuilder $queryBuilder, QueryBuilder $countQb = null) { - $this->qb = $event->getQueryBuilder(); - - $selectParts = $this->qb->getDQLPart('select'); - $stringSelectParts = []; - foreach ($selectParts as $selectPart) { - $stringSelectParts[] = (string) $selectPart; + $filters = $parameters->get('_filter'); + if (!$filters || !is_array($filters)) { + return; + } + $this->filterJoins = []; + $groupByFilters = ['cc', 'bcc', 'to', 'folders', 'folder', 'mailbox']; + + // As now optimizer could not automatically remove joins for these filters + // (they do not affect number of rows) + // adding group by statement + if (array_intersect_key($filters, array_flip($groupByFilters))) { + // CountQb doesn't need group by statement cos it already added in grid config + $queryBuilder->addGroupBy('eu.id'); + } + $rFilters = [ + 'cc' => ['r_cc', 'WITH', 'r_cc.type = :ccType', ['ccType' => 'cc']], + 'bcc' => ['r_bcc', 'WITH', 'r_bcc.type = :bccType', ['bccType' => 'bcc']], + 'to' => ['r_to', null, null, []], + ]; + $rParams = []; + // Add join for each filter which is based on e.recipients table + foreach ($rFilters as $rKey => $rFilter) { + if (array_key_exists($rKey, $filters)) { + $queryBuilder->leftJoin('e.recipients', $rFilter[0], $rFilter[1], $rFilter[2]); + $countQb->leftJoin('e.recipients', $rFilter[0], $rFilter[1], $rFilter[2]); + $rParams = array_merge($rParams, $rFilter[3]); + $this->filterJoins['eu'][] = $rFilter[0]; + } + } + foreach ($rParams as $rParam => $rParamValue) { + $queryBuilder->setParameter($rParam, $rParamValue); + $countQb->setParameter($rParam, $rParamValue); + } + $fFilters = ['folder', 'folders', 'mailbox']; + if (array_intersect_key($filters, array_flip($fFilters))) { + $queryBuilder->leftJoin('eu.folders', 'f'); + $this->filterJoins['eu'][] = 'f'; } - $this->select = implode(', ', $stringSelectParts); - - $this->qb->select('eu.id'); } + /** - * @param OrmResultAfter $event + * + * @param QueryBuilder $qb + * @param $part */ - public function onResultAfter(OrmResultAfter $event) + protected function removeGroupByPart(QueryBuilder $qb, $part) { - $originalRecords = $event->getRecords(); - if (!$originalRecords) { - return; - } - - $ids = []; - foreach ($originalRecords as $record) { - $ids[] = $record->getValue('id'); - } - - $this->qb - ->select($this->select) - ->resetDQLPart('groupBy') - ->where($this->qb->expr()->in('eu.id', ':ids')) - ->setMaxResults(null) - ->setFirstResult(null) - ->setParameter('ids', $ids); - QueryUtils::removeUnusedParameters($this->qb); - $result = $this->qb - ->getQuery() - ->getResult(); - - $records = []; - foreach ($result as $row) { - $records[] = new ResultRecord($row); + $groupByParts = $qb->getDQLPart('groupBy'); + $qb->resetDQLPart('groupBy'); + /** @var GroupBy $groupByPart */ + foreach ($groupByParts as $i => $groupByPart) { + $newGroupByPart = []; + foreach ($groupByPart->getParts() as $j => $val) { + if ($val !== $part) { + $newGroupByPart[] = $val; + } + } + if ($newGroupByPart) { + call_user_func_array([$qb, 'addGroupBy'], $newGroupByPart); + } } - $event->setRecords($records); } /** - * Add join for query just if filter used. For performance optimization - BAP-10674 - * - * @param ParameterBag $parameters - * @param QueryBuilder $queryBuilder + * @param QueryBuilder $qb + * @param array $rootAndAliases ['root1' => ['aliasToRemove1', 'aliasToRemove2', ...], ...] */ - protected function prepareQueryToFilter($parameters, $queryBuilder) + protected function removeJoinByRootAndAliases(QueryBuilder $qb, array $rootAndAliases) { - $filters = $parameters->get('_filter'); - if ($filters && array_key_exists('cc', $filters)) { - $queryBuilder->leftJoin('e.recipients', 'r_cc', 'WITH', "r_cc.type = 'cc'"); - } - if ($filters && array_key_exists('bcc', $filters)) { - $queryBuilder->leftJoin('e.recipients', 'r_bcc', 'WITH', "r_bcc.type = 'bcc'"); + $joins = $qb->getDQLPart('join'); + + /** @var Join $join */ + foreach ($joins as $root => $rJoins) { + if (!empty($rootAndAliases[$root]) && is_array($rootAndAliases[$root])) { + foreach ($rJoins as $key => $join) { + if (in_array($join->getAlias(), $rootAndAliases[$root], true)) { + unset($rJoins[$key]); + } + } + } + $qb->add('join', [$root => $rJoins]); } } } diff --git a/src/Oro/Bundle/EmailBundle/Filter/ChoiceMessageTypeFilter.php b/src/Oro/Bundle/EmailBundle/Filter/ChoiceMessageTypeFilter.php index db29d069a09..f4322cb5c91 100644 --- a/src/Oro/Bundle/EmailBundle/Filter/ChoiceMessageTypeFilter.php +++ b/src/Oro/Bundle/EmailBundle/Filter/ChoiceMessageTypeFilter.php @@ -43,11 +43,16 @@ public function apply(FilterDatasourceAdapterInterface $ds, $data) if (!$data) { return false; } + $bothValuesSelected = in_array(FolderType::INBOX, $data['value'], true) && + in_array(FolderType::SENT, $data['value'], true); - if (in_array(FolderType::INBOX, $data['value']) && in_array(FolderType::SENT, $data['value'])) { + $noValueSelected = !in_array(FolderType::INBOX, $data['value'], true) && + !in_array(FolderType::SENT, $data['value'], true); + + if ($bothValuesSelected) { $data['value'] = []; return parent::apply($ds, $data); - } elseif (!in_array(FolderType::INBOX, $data['value']) && !in_array(FolderType::SENT, $data['value'])) { + } elseif ($noValueSelected) { return parent::apply($ds, $data); } @@ -55,25 +60,31 @@ public function apply(FilterDatasourceAdapterInterface $ds, $data) return false; } - $qb = $ds->getQueryBuilder(); - if (in_array(FolderType::INBOX, $data['value'])) { - $this->applyInboxFilter($qb); + if (in_array(FolderType::INBOX, $data['value'], true)) { + $this->applyInboxFilter($ds); } else { - $this->applySentFilter($qb); + $this->applySentFilter($ds); } return true; } /** - * @param QueryBuilder $qb + * @param OrmFilterDatasourceAdapter $ds */ - protected function applyInboxFilter(QueryBuilder $qb) + protected function applyInboxFilter(OrmFilterDatasourceAdapter $ds) { - $qb - ->leftJoin('e.fromEmailAddress', '_fea') - ->leftJoin(sprintf('_fea.%s', $this->getUserOwnerFieldName()), '_fo') - ->leftJoin('eu.owner', '_eo') + $qb = $ds->getQueryBuilder(); + $subQb = clone $qb; + $subQb + ->resetDQLPart('where') + ->resetDQLPart('orderBy') + ->select('eu.id') + ->leftJoin('eu.folders', '_cmtf_folders') + ->leftJoin('e.fromEmailAddress', '_cmtf_fea') + ->leftJoin(sprintf('_cmtf_fea.%s', $this->getUserOwnerFieldName()), '_cmtf_fo') + ->leftJoin('eu.owner', '_cmtf_eo') + ->andWhere('eu.id = eu.id') ->andWhere( $qb->expr()->orX( $qb->expr()->in('f.type', ':incoming_types'), @@ -81,47 +92,71 @@ protected function applyInboxFilter(QueryBuilder $qb) $qb->expr()->notIn('f.type', ':outcoming_types'), $qb->expr()->orX( $qb->expr()->andX( - $qb->expr()->isNull('_eo.id'), - $qb->expr()->isNotNull('_fo.id') + $qb->expr()->isNull('_cmtf_eo.id'), + $qb->expr()->isNotNull('_cmtf_fo.id') ), $qb->expr()->andX( - $qb->expr()->isNotNull('_eo.id'), - $qb->expr()->isNull('_fo.id') + $qb->expr()->isNotNull('_cmtf_eo.id'), + $qb->expr()->isNull('_cmtf_fo.id') ), $qb->expr()->andX( - $qb->expr()->isNotNull('_eo.id'), - $qb->expr()->isNotNull('_fo.id'), - $qb->expr()->neq('_fo.id', '_eo.id') + $qb->expr()->isNotNull('_cmtf_eo.id'), + $qb->expr()->isNotNull('_cmtf_fo.id'), + $qb->expr()->neq('_cmtf_fo.id', '_cmtf_eo.id') ) ) ) ) - ) + ); + + list($dql, $replacements) = $this->createDQLWithReplacedAliases($ds, $subQb); + + $replacedFieldExpr = sprintf('%s.%s', $replacements['eu'], 'id'); + $oldExpr = sprintf('%1$s = %1$s', $replacedFieldExpr); + $newExpr = sprintf('%s = eu.id', $replacedFieldExpr); + $dql = strtr($dql, [$oldExpr => $newExpr]); + $qb ->setParameter('outcoming_types', FolderType::outcomingTypes()) - ->setParameter('incoming_types', FolderType::incomingTypes()); + ->setParameter('incoming_types', FolderType::incomingTypes()) + ->andWhere($qb->expr()->exists($dql)); } /** - * @param QueryBuilder $qb + * @param OrmFilterDatasourceAdapter $ds */ - protected function applySentFilter(QueryBuilder $qb) + protected function applySentFilter(OrmFilterDatasourceAdapter $ds) { - $qb - ->leftJoin('e.fromEmailAddress', '_fea') - ->leftJoin(sprintf('_fea.%s', $this->getUserOwnerFieldName()), '_fo') - ->leftJoin('eu.owner', '_eo') + $qb = $ds->getQueryBuilder(); + $subQb = clone $qb; + $subQb + ->resetDQLPart('where') + ->resetDQLPart('orderBy') + ->select('eu.id') + ->leftJoin('eu.folders', '_cmtf_folders') + ->leftJoin('e.fromEmailAddress', '_cmtf_fea') + ->leftJoin(sprintf('_cmtf_fea.%s', $this->getUserOwnerFieldName()), '_cmtf_fo') + ->leftJoin('eu.owner', '_cmtf_eo') + ->andWhere('eu.id = eu.id') ->andWhere( $qb->expr()->orX( - $qb->expr()->in('f.type', ':outcoming_types'), + $qb->expr()->in('_cmtf_folders.type', ':outcoming_types'), $qb->expr()->andX( - $qb->expr()->notIn('f.type', ':incoming_types'), - $qb->expr()->isNotNull('_eo.id'), - $qb->expr()->eq('_fo.id', '_eo.id') + $qb->expr()->notIn('_cmtf_folders.type', ':incoming_types'), + $qb->expr()->isNotNull('_cmtf_eo.id'), + $qb->expr()->eq('_cmtf_fo.id', '_cmtf_eo.id') ) ) - ) - ->setParameter('outcoming_types', FolderType::outcomingTypes()) - ->setParameter('incoming_types', FolderType::incomingTypes()); + ); + list($dql, $replacements) = $this->createDQLWithReplacedAliases($ds, $subQb); + + $replacedFieldExpr = sprintf('%s.%s', $replacements['eu'], 'id'); + $oldExpr = sprintf('%1$s = %1$s', $replacedFieldExpr); + $newExpr = sprintf('%s = eu.id', $replacedFieldExpr); + $dql = strtr($dql, [$oldExpr => $newExpr]); + $qb + ->setParameter('outcoming_types', FolderType::outcomingTypes()) + ->setParameter('incoming_types', FolderType::incomingTypes()) + ->andWhere($qb->expr()->exists($dql)); } /** diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateRichTextType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateRichTextType.php index 55aa57afb6e..e6533751e6c 100644 --- a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateRichTextType.php +++ b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateRichTextType.php @@ -5,22 +5,41 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; +use Oro\Bundle\FormBundle\Form\DataTransformer\SanitizeHTMLTransformer; use Oro\Bundle\EmailBundle\Form\DataTransformer\EmailTemplateTransformer; class EmailTemplateRichTextType extends AbstractType { const NAME = 'oro_email_template_rich_text'; + /** @var string */ + protected $cacheDir; + + /** + * @param string $cacheDir + */ + public function __construct($cacheDir) + { + $this->cacheDir = $cacheDir; + } + /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { - $templateTransformer = new EmailTemplateTransformer(); - // sanitize transformer is already added in the parent type - $templateTransformer->setSanitize(false); - // append template transformer to run after parent type transformers - $builder->addModelTransformer($templateTransformer, true); + if (null !== $options['wysiwyg_options']['valid_elements']) { + $templateTransformer = new EmailTemplateTransformer( + new SanitizeHTMLTransformer( + $options['wysiwyg_options']['valid_elements'], + $this->cacheDir + ) + ); + // sanitize transformer is already added in the parent type + $templateTransformer->setSanitize(false); + // append template transformer to run after parent type transformers + $builder->addModelTransformer($templateTransformer, true); + } } /** diff --git a/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php b/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php index a9b8d7a6810..c4cfa1a5cc0 100644 --- a/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php +++ b/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php @@ -27,7 +27,7 @@ use Oro\Bundle\EmailBundle\Migrations\Schema\v1_22\OroEmailBundle as OroEmailBundle122; use Oro\Bundle\EmailBundle\Migrations\Schema\v1_23\OroEmailBundle as OroEmailBundle123; use Oro\Bundle\EmailBundle\Migrations\Schema\v1_24\OroEmailBundle as OroEmailBundle124; -use Oro\Bundle\EmailBundle\Migrations\Schema\v1_25\OroEmailBundle as OroEmailBundle125; +use Oro\Bundle\EmailBundle\Migrations\Schema\v1_26\OroEmailBundle as OroEmailBundle126; /** * Class OroEmailBundleInstaller @@ -42,7 +42,7 @@ class OroEmailBundleInstaller implements Installation */ public function getMigrationVersion() { - return 'v1_25'; + return 'v1_26'; } /** @@ -116,6 +116,6 @@ public function up(Schema $schema, QueryBag $queries) OroEmailBundle123::oroEmailTable($schema); OroEmailBundle124::removeIndex($schema); - OroEmailBundle125::addEmailUserMailboxOwnerSeenIndex($schema); + OroEmailBundle126::addEmailUserMailboxOwnerSeenIndex($schema); } } diff --git a/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_26/OroEmailBundle.php b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_26/OroEmailBundle.php new file mode 100644 index 00000000000..fdbc7413e42 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_26/OroEmailBundle.php @@ -0,0 +1,39 @@ +getTable('oro_email_user'); + $table->dropIndex('mailbox_seen_idx'); + } + + /** + * @param Schema $schema + */ + public static function addEmailUserMailboxOwnerSeenIndex(Schema $schema) + { + $table = $schema->getTable('oro_email_user'); + $table->addIndex(['is_seen', 'mailbox_owner_id'], 'seen_idx'); + $table->addIndex(['received', 'is_seen', 'mailbox_owner_id'], 'received_idx'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml index a390962f948..c7daf39d4b4 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml @@ -78,13 +78,29 @@ datagrid: acl_resource: oro_email_email_user_view source: type: orm + count_query: + select: + - eu.id + join: + left: + - + join: eu.email + alias: e + - + join: eu.folders + alias: f + - + join: f.origin + alias: o + where: + and: + - o.isActive = true + groupBy: eu.id query: select: - partial eu.{id, email} - e - - partial f.{id, type} - eb.bodyContent AS body_content - - r_to - > (SELECT COUNT(_ec.id) FROM OroEmailBundle:Email _ec @@ -110,25 +126,18 @@ datagrid: - join: eu.email alias: e - - - join: eu.mailboxOwner - alias: mb - - - join: e.recipients - alias: r_to - - - join: eu.folders - alias: f - - - join: f.origin - alias: o - join: e.emailBody alias: eb where: and: - - o.isActive = true - groupBy: e.sentAt, eu.id + - > + EXISTS ( + SELECT 1 FROM OroEmailBundle:EmailUser act_eu + JOIN act_eu.folders act_f + JOIN act_f.origin act_o + WHERE act_o.isActive = true AND eu.id = act_eu.id + ) columns: contacts: @@ -175,10 +184,16 @@ datagrid: email-grid: extends: base-email-grid source: + count_query: + where: + and: + - e.head = true query: where: and: - e.head = true + # joins for filters to, cc, bcc, folders, folder and mailbox processes dynamically in + # Oro\Bundle\EmailBundle\EventListener\Datagrid\EmailGridListener for performance reasons filters: columns: subject: { type: string, data_name: e.subject } @@ -325,6 +340,8 @@ datagrid: entityHint: email toolbarOptions: hide: true + pagination: + onePage: true pageSize: items: [10] default_per_page: 10 diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/form.yml b/src/Oro/Bundle/EmailBundle/Resources/config/form.yml index 3e8f69e520b..9449b0f8729 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/form.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/form.yml @@ -102,6 +102,8 @@ services: oro_email.form.type.emailtemplate_rich_text: class: Oro\Bundle\EmailBundle\Form\Type\EmailTemplateRichTextType + arguments: + - %kernel.cache_dir% tags: - { name: form.type, alias: oro_email_template_rich_text } diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml index e3b7c624e35..d1fc93d7acc 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml @@ -559,7 +559,6 @@ services: tags: - { name: kernel.event_listener, event: oro_datagrid.datagrid.build.after.base-email-grid, method: onBuildAfter } - { name: kernel.event_listener, event: oro_datagrid.orm_datasource.result.before_query.base-email-grid, method: onResultBeforeQuery, priority: -255 } - - { name: kernel.event_listener, event: oro_datagrid.orm_datasource.result.after.base-email-grid, method: onResultAfter, priority: 255 } oro_email.listener.datagrid.activity: class: %oro_email.listener.datagrid.activity.class% diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml index c13fa24beff..dc98f45b0b5 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml @@ -14,7 +14,10 @@ oro: recently_used: Recently used contexts: Contexts filter.inactive: inactive - controller.emailtemplate.saved.message: "Template saved" + controller: + emailtemplate.saved.message: "Template saved" + job_scheduled.message: "The job has been added to the queue %link%" + job_progress: Check progress form: choose_template: "" add_signature: Add Signature @@ -29,7 +32,7 @@ oro: tooltip: attachment_sync: enable: Enable load attachments on email sync. - max_size: Maximum sync attacment size, Mb. Attachments with exceeding size will not be downloaded. To unlimit size set to 0. + max_size: Maximum sync attachment size, Mb. Attachments with exceeding size will not be downloaded. To unlimit size set to 0. preview_limit: Limit to show preview for attachments (thumbnail for images and big file icon for other files). Set to 0 to see a list with file names only. variable.not.found: N/A @@ -56,6 +59,7 @@ oro: choices: auto.label: Auto manual.label: Manual + remove_larger_attachments.label: Remove large attachments reply_configuration: label: Reply diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig index f8c3bae58f3..932aacdc4c0 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig @@ -1,3 +1,16 @@ +{% block _email_configuration_oro_email___attachment_sync_max_size_value_widget %} + {{ form_widget(form) }} + {% if is_granted('oro_config_system') %} + + {{ 'oro.email.system_configuration.attachment_configuration.remove_larger_attachments.label'|trans }} + + {% endif %} +{% endblock %} + {% block _oro_email_autoresponserule_conditions_entry_field_row %} {{ form_widget(form) }} {% endblock %} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php index bfb4c65314e..c778afdbf66 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php @@ -141,7 +141,7 @@ public function addAttachmentProvider() 'data' => [ 'contentTransferEncoding' => 'base64', 'embeddedContentId' => 123, - 'contentSize' => 1024 * 1024 * 5, + 'contentSize' => 1000 * 1000 * 5, ], 'configSyncEnabled' => true, 'configSyncMaxSize' => 0.1, @@ -151,7 +151,7 @@ public function addAttachmentProvider() 'data' => [ 'contentTransferEncoding' => 'base64', 'embeddedContentId' => 123, - 'contentSize' => 1024 * 1024 * 0.49 * 4 / 3, + 'contentSize' => 1000 * 1000 * 0.49 * 4 / 3, ], 'configSyncEnabled' => true, 'configSyncMaxSize' => 0.5, diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateRichTextTypeTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateRichTextTypeTest.php new file mode 100644 index 00000000000..2f226293523 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTemplateRichTextTypeTest.php @@ -0,0 +1,41 @@ +type = new EmailTemplateRichTextType('/tmp'); + } + + protected function tearDown() + { + unset($this->type); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilderInterface') + ->disableOriginalConstructor() + ->getMock(); + $builder->expects($this->once()) + ->method('addModelTransformer'); + $options = [ + 'wysiwyg_options' => [ + 'valid_elements' => [ + 'a', + ], + ], + ]; + + $this->type->buildForm($builder, $options); + } +} diff --git a/src/Oro/Bundle/EmbeddedFormBundle/Resources/public/js/embed.form.js b/src/Oro/Bundle/EmbeddedFormBundle/Resources/public/js/embed.form.js index 5a1668f314a..e44278ef2d3 100644 --- a/src/Oro/Bundle/EmbeddedFormBundle/Resources/public/js/embed.form.js +++ b/src/Oro/Bundle/EmbeddedFormBundle/Resources/public/js/embed.form.js @@ -79,6 +79,11 @@ var ORO = (function(ORO) { onSubmit: function(e) { e.preventDefault(); + var button = this.container.querySelector('button'); + if (button) { + button.disable(); + } + this.ajax(this.options.url, { method: 'POST', data: new FormData(e.target), diff --git a/src/Oro/Bundle/EmbeddedFormBundle/Resources/views/layouts/embedded_default/form.html.twig b/src/Oro/Bundle/EmbeddedFormBundle/Resources/views/layouts/embedded_default/form.html.twig index 311a77778ba..1ce3d65670b 100644 --- a/src/Oro/Bundle/EmbeddedFormBundle/Resources/views/layouts/embedded_default/form.html.twig +++ b/src/Oro/Bundle/EmbeddedFormBundle/Resources/views/layouts/embedded_default/form.html.twig @@ -16,6 +16,19 @@
{{ block('container_widget') }} + {% block javascript %} + + {% endblock javascript %}
{% endblock %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Tools/Fixtures/collections_with_plural_names.txt b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Tools/Fixtures/collections_with_plural_names.txt new file mode 100644 index 00000000000..4dc3db5335f --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Tools/Fixtures/collections_with_plural_names.txt @@ -0,0 +1,41 @@ +namespace Test; + +class Entity implements \Oro\Bundle\EntityExtendBundle\Entity\ExtendEntityInterface +{ + /** + * @deprecated since 1.10. Use removeOwner instead + */ + public function removeOwners($value) + { + $this->removeOwner($value); + } + + public function removeOwner($value) + { + if ($this->owners && $this->owners->contains($value)) { + $this->owners->removeElement($value); + $value->removeTarget($this); + } + } + + /** + * @deprecated since 1.10. Use addOwner instead + */ + public function addOwners($value) + { + $this->addOwner($value); + } + + public function addOwner($value) + { + if (!$this->owners->contains($value)) { + $this->owners->add($value); + $value->addTarget($this); + } + } + + public function __construct() + { + $this->owners = new \Doctrine\Common\Collections\ArrayCollection(); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Tools/GeneratorExtensions/ExtendEntityGeneratorExtensionTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Tools/GeneratorExtensions/ExtendEntityGeneratorExtensionTest.php index ba454708dbe..5594969cb40 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Tools/GeneratorExtensions/ExtendEntityGeneratorExtensionTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Tools/GeneratorExtensions/ExtendEntityGeneratorExtensionTest.php @@ -147,6 +147,24 @@ public function testCollections() $this->assertGeneration('collections.txt', $schema); } + public function testCollectionsWithPluralNames() + { + $schema = [ + 'type' => 'Extend', + 'property' => [], + 'relation' => [], + 'default' => [], + 'addremove' => [ + 'owners' => [ + 'self' => 'owners', + 'is_target_addremove' => true, + 'target' => 'targets', + ], + ] + ]; + $this->assertGeneration('collections_with_plural_names.txt', $schema); + } + public function testRelations() { $schema = [ diff --git a/src/Oro/Bundle/EntityExtendBundle/Tools/GeneratorExtensions/ExtendEntityGeneratorExtension.php b/src/Oro/Bundle/EntityExtendBundle/Tools/GeneratorExtensions/ExtendEntityGeneratorExtension.php index ba22df543f6..17718845343 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tools/GeneratorExtensions/ExtendEntityGeneratorExtension.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tools/GeneratorExtensions/ExtendEntityGeneratorExtension.php @@ -7,6 +7,7 @@ use CG\Generator\PhpProperty; use Doctrine\Common\Inflector\Inflector; +use Symfony\Component\PropertyAccess\StringUtil; /** * The main extension of the entity generator. This extension is responsible for generate extend entity skeleton @@ -44,6 +45,7 @@ public function generate(array $schema, PhpClass $class) $this->generateProperties('relation', $schema, $class); $this->generateProperties('default', $schema, $class); $this->generateCollectionMethods($schema, $class); + $this->generateDeprecatedCollectionMethods($schema, $class); } /** @@ -206,6 +208,44 @@ protected function generateCollectionMethods(array $schema, PhpClass $class) } } + /** + * @param array $schema + * @param PhpClass $class + */ + protected function generateDeprecatedCollectionMethods(array $schema, PhpClass $class) + { + foreach ($schema['addremove'] as $fieldName => $config) { + $selfFieldName = $config['self']; + $addMethodName = $this->generateAddMethodName($selfFieldName); + $removeMethodName = $this->generateRemoveMethodName($selfFieldName); + $deprecatedAddMethodName = $this->generateDeprecatedAddMethodName($selfFieldName); + $deprecatedRemoveMethodName = $this->generateDeprecatedRemoveMethodName($selfFieldName); + + if ($deprecatedAddMethodName !== $addMethodName) { + $class->setMethod( + $this->generateClassMethod( + $deprecatedAddMethodName, + "\$this->{$addMethodName}(\$value);", + ['value'] + )->setDocblock( + "/**\n * @deprecated since 1.10. Use " . $addMethodName . " instead\n */" + ) + ); + } + if ($deprecatedRemoveMethodName !== $removeMethodName) { + $class->setMethod( + $this->generateClassMethod( + $deprecatedRemoveMethodName, + "\$this->{$removeMethodName}(\$value);", + ['value'] + )->setDocblock( + "/**\n * @deprecated since 1.10. Use " . $removeMethodName . " instead\n */" + ) + ); + } + } + } + /** * @param string $fieldName * @return string @@ -230,7 +270,7 @@ protected function generateSetMethodName($fieldName) */ protected function generateAddMethodName($fieldName) { - return 'add' . ucfirst(Inflector::camelize($fieldName)); + return 'add' . ucfirst($this->getSingular($fieldName)); } /** @@ -238,7 +278,40 @@ protected function generateAddMethodName($fieldName) * @return string */ protected function generateRemoveMethodName($fieldName) + { + return 'remove' . ucfirst($this->getSingular($fieldName)); + } + + /** + * @param string $fieldName + * @return string + */ + protected function generateDeprecatedAddMethodName($fieldName) + { + return 'add' . ucfirst(Inflector::camelize($fieldName)); + } + + /** + * @param string $fieldName + * @return string + */ + protected function generateDeprecatedRemoveMethodName($fieldName) { return 'remove' . ucfirst(Inflector::camelize($fieldName)); } + + /** + * @param string $fieldName + * + * @return string + */ + protected function getSingular($fieldName) + { + $singular = StringUtil::singularify(Inflector::classify($fieldName)); + if (is_array($singular)) { + $singular = reset($singular); + } + + return $singular; + } } diff --git a/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php b/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php index eb74de8e53d..bd5b7639478 100644 --- a/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php +++ b/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php @@ -97,18 +97,21 @@ public function apply(FilterDatasourceAdapterInterface $ds, $data) } if ($relatedJoin) { + /** @var OrmFilterDatasourceAdapter $ds */ $qb = $ds->getQueryBuilder(); $fieldsExprs = $this->createConditionFieldExprs($qb); $subExprs = []; + $groupBy = implode(', ', $this->getSelectFieldFromGroupBy($qb)); + foreach ($fieldsExprs as $fieldExpr) { $subQb = clone $qb; $subQb - ->resetDqlPart('orderBy') + ->resetDQLPart('orderBy') + ->resetDQLPart('where') ->select($fieldExpr) ->andWhere($comparisonExpr) ->andWhere(sprintf('%1$s = %1$s', $fieldExpr)); - $groupBy = implode(', ', $this->getSelectFieldFromGroupBy($qb)); if ($groupBy) { // replace aliases from SELECT by expressions, since SELECT clause is changed $subQb->groupBy($groupBy); diff --git a/src/Oro/Bundle/FilterBundle/Form/Type/Filter/EnumFilterType.php b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/EnumFilterType.php index 62472cf8624..a96a862e724 100644 --- a/src/Oro/Bundle/FilterBundle/Form/Type/Filter/EnumFilterType.php +++ b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/EnumFilterType.php @@ -16,10 +16,10 @@ class EnumFilterType extends AbstractMultiChoiceType { const NAME = 'oro_enum_filter'; - const TYPE_IN = 1; - const TYPE_NOT_IN = 2; - const EQUAL = 3; - const NOT_EQUAL = 4; + const TYPE_IN = '1'; + const TYPE_NOT_IN = '2'; + const EQUAL = '3'; + const NOT_EQUAL = '4'; /** * @var EnumValueProvider diff --git a/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php b/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php index 7add3ad93b2..aaed2f433d1 100644 --- a/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php +++ b/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php @@ -37,12 +37,12 @@ class OrmFilterExtension extends AbstractExtension /** * @param ConfigurationProvider $configurationProvider - * @param TranslatorInterface $translator + * @param TranslatorInterface $translator */ public function __construct(ConfigurationProvider $configurationProvider, TranslatorInterface $translator) { $this->configurationProvider = $configurationProvider; - $this->translator = $translator; + $this->translator = $translator; } /** @@ -85,6 +85,11 @@ public function visitDatasource(DatagridConfiguration $config, DatasourceInterfa $filters = $this->getFiltersToApply($config); $values = $this->getValuesToApply($config); /** @var OrmDatasource $datasource */ + $countQb = $datasource->getCountQb(); + $countQbAdapter = null; + if ($countQb) { + $countQbAdapter = new OrmFilterDatasourceAdapter($countQb); + } $datasourceAdapter = new OrmFilterDatasourceAdapter($datasource->getQueryBuilder()); foreach ($filters as $filter) { @@ -105,6 +110,9 @@ public function visitDatasource(DatagridConfiguration $config, DatasourceInterfa $data['value']['end_original'] = $value['value']['end']; } $filter->apply($datasourceAdapter, $data); + if ($countQbAdapter) { + $filter->apply($countQbAdapter, $data); + } } } } @@ -132,9 +140,9 @@ public function visitMetadata(DatagridConfiguration $config, MetadataObject $dat if (!$lazy) { $filter->resolveOptions(); } - $name = $filter->getName(); - $value = $this->getFilterValue($values, $name); - $initialValue = $this->getFilterValue($initialValues, $name); + $name = $filter->getName(); + $value = $this->getFilterValue($values, $name); + $initialValue = $this->getFilterValue($initialValues, $name); $filtersState = $this->updateFilterStateEnabled($name, $filtersParams, $filtersState); $filtersState = $this->updateFiltersState($filter, $value, $filtersState); $initialFiltersState = $this->updateFiltersState($filter, $initialValue, $initialFiltersState); @@ -144,7 +152,7 @@ public function visitMetadata(DatagridConfiguration $config, MetadataObject $dat $filtersMetaData[] = array_merge( $metadata, [ - 'label' => $metadata[FilterUtility::TRANSLATABLE_KEY] + 'label' => $metadata[FilterUtility::TRANSLATABLE_KEY] ? $this->translator->trans($metadata['label']) : $metadata['label'], 'cacheId' => $this->getFilterCacheId($rawConfig, $metadata), @@ -284,6 +292,7 @@ protected function getValuesToApply(DatagridConfiguration $config, $readParamete return $defaultFilters; } else { $currentFilters = $this->getParameters()->get(self::FILTER_ROOT_PARAM, []); + return array_replace($defaultFilters, $currentFilters); } } diff --git a/src/Oro/Bundle/FormBundle/Resources/public/js/app/views/editor/select-editor-view.js b/src/Oro/Bundle/FormBundle/Resources/public/js/app/views/editor/select-editor-view.js index 51f0e355091..eea2385a447 100644 --- a/src/Oro/Bundle/FormBundle/Resources/public/js/app/views/editor/select-editor-view.js +++ b/src/Oro/Bundle/FormBundle/Resources/public/js/app/views/editor/select-editor-view.js @@ -64,6 +64,7 @@ define(function(require) { var SelectEditorView; var TextEditorView = require('./text-editor-view'); var _ = require('underscore'); + var $ = require('jquery'); require('jquery.select2'); SelectEditorView = TextEditorView.extend(/** @exports SelectEditorView.prototype */{ @@ -154,11 +155,6 @@ define(function(require) { e.preventDefault(); _this.$('input[name=value]').inputWidget('close'); _this.onGenericEnterKeydown(e); - } else if (!select2options.multiple) { - _this.$('input[name=value]').on('select2-selecting', function(event) { - _this.$('input[name=value]').val(event.val); - _this.onGenericEnterKeydown(e); - }); } break; case _this.TAB_KEY_CODE: @@ -275,7 +271,11 @@ define(function(require) { if (this._isSelection) { this.$('.select2-focused').focus(); } else if (!select2 || !select2.opened()) { - SelectEditorView.__super__.onFocusout.call(this, e); + _.defer(_.bind(function() { + if (!this.disposed && !$.contains(this.el, document.activeElement)) { + SelectEditorView.__super__.onFocusout.call(this, e); + } + }, this)); } }, diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php index 8e8868f2363..2191e623470 100644 --- a/src/Oro/Bundle/FormBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\FormBundle\Tests\Unit\Validator\Constraints; +use Doctrine\Common\Collections\AbstractLazyCollection; + use Oro\Bundle\FormBundle\Validator\Constraints\ContainsPrimaryValidator; class ContainsPrimaryValidatorTest extends \PHPUnit_Framework_TestCase @@ -17,6 +19,16 @@ public function testValidateException() $validator->validate(false, $constraint); } + public function testShouldKeepLazyCollectionUninitialized() + { + /** @var AbstractLazyCollection $collection */ + $collection = $this->getMockForAbstractClass(AbstractLazyCollection::class); + $validator = new ContainsPrimaryValidator(); + $validator->validate($collection, $this->getMock('Symfony\Component\Validator\Constraint')); + + $this->assertFalse($collection->isInitialized()); + } + /** * @dataProvider validItemsDataProvider * @param array $items diff --git a/src/Oro/Bundle/FormBundle/Validator/Constraints/ContainsPrimaryValidator.php b/src/Oro/Bundle/FormBundle/Validator/Constraints/ContainsPrimaryValidator.php index 0f1ee52551c..bf55caa207c 100644 --- a/src/Oro/Bundle/FormBundle/Validator/Constraints/ContainsPrimaryValidator.php +++ b/src/Oro/Bundle/FormBundle/Validator/Constraints/ContainsPrimaryValidator.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\FormBundle\Validator\Constraints; +use Doctrine\Common\Collections\AbstractLazyCollection; + use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -15,6 +17,9 @@ public function validate($value, Constraint $constraint) if (!is_array($value) && !($value instanceof \Traversable && $value instanceof \ArrayAccess)) { throw new UnexpectedTypeException($value, 'array or Traversable and ArrayAccess'); } + if ($value instanceof AbstractLazyCollection && !$value->isInitialized()) { + return; + } $primaryItemsNumber = 0; $totalItemsNumber = 0; diff --git a/src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php b/src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php index 968d21f9893..bcd5fe92905 100644 --- a/src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php +++ b/src/Oro/Bundle/OrganizationBundle/Entity/Manager/BusinessUnitManager.php @@ -309,4 +309,12 @@ protected function getBuWithChildTree($businessUnitId, $tree) } } } + + /** + * @return int + */ + protected function getOrganizationContextId() + { + return $this->securityFacade->getOrganization()->getId(); + } } diff --git a/src/Oro/Bundle/OrganizationBundle/Resources/config/oro/api.yml b/src/Oro/Bundle/OrganizationBundle/Resources/config/oro/api.yml index 26721a752e5..3fee33898b4 100644 --- a/src/Oro/Bundle/OrganizationBundle/Resources/config/oro/api.yml +++ b/src/Oro/Bundle/OrganizationBundle/Resources/config/oro/api.yml @@ -2,6 +2,13 @@ oro_api: entities: Oro\Bundle\OrganizationBundle\Entity\BusinessUnit: delete_handler: oro_organization.business_unit.handler.delete + fields: + createdAt: + form_options: + mapped: false + updatedAt: + form_options: + mapped: false actions: delete: exclude: false # set manually because this entity is marked as a dictionary diff --git a/src/Oro/Bundle/ReminderBundle/Resources/config/validation.yml b/src/Oro/Bundle/ReminderBundle/Resources/config/validation.yml index 296c8e155b2..16ba672f58b 100644 --- a/src/Oro/Bundle/ReminderBundle/Resources/config/validation.yml +++ b/src/Oro/Bundle/ReminderBundle/Resources/config/validation.yml @@ -3,6 +3,7 @@ Oro\Bundle\ReminderBundle\Entity\Reminder: method: - NotBlank: ~ interval: + - Valid: ~ - NotBlank: ~ Oro\Bundle\ReminderBundle\Model\ReminderInterval: diff --git a/src/Oro/Bundle/ReminderBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/ReminderBundle/Resources/views/Form/fields.html.twig index 615fa1e43b3..f0cf4f373f8 100644 --- a/src/Oro/Bundle/ReminderBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/ReminderBundle/Resources/views/Form/fields.html.twig @@ -7,14 +7,22 @@ {% block oro_reminder_widget %}
-
- {{ form_widget(form.method) }} -
-
- {{ form_widget(form.interval.number) }} -
-
- {{ form_widget(form.interval.unit) }} +
+
+
+ {{ form_widget(form.method) }} +
+
+ {{ form_widget(form.interval.number) }} +
+
+ {{ form_widget(form.interval.unit) }} +
+
+ {{ form_errors(form.method) }} + {{ form_errors(form.interval.number) }} + {{ form_errors(form.interval.unit) }} + {{ form_errors(form) }}
{% endblock %} diff --git a/src/Oro/Bundle/SecurityBundle/Http/Firewall/ExceptionListener.php b/src/Oro/Bundle/SecurityBundle/Http/Firewall/ExceptionListener.php new file mode 100644 index 00000000000..9d8f2c39682 --- /dev/null +++ b/src/Oro/Bundle/SecurityBundle/Http/Firewall/ExceptionListener.php @@ -0,0 +1,64 @@ +providerKey = $providerKey; + } + + /** + * {@inheritdoc} + */ + protected function setTargetPath(Request $request) + { + if (!$request->hasSession() || + !$request->isMethodSafe() || + ($request->isXmlHttpRequest() && !$request->headers->get(ResponseHashnavListener::HASH_NAVIGATION_HEADER)) + ) { + return; + } + + $request->getSession()->set('_security.'.$this->providerKey.'.target_path', $request->getUri()); + } +} diff --git a/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml b/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml index c44e08df389..70ae08b39ac 100644 --- a/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml @@ -1,4 +1,5 @@ parameters: + security.exception_listener.class: Oro\Bundle\SecurityBundle\Http\Firewall\ExceptionListener oro_security.security_facade.class: Oro\Bundle\SecurityBundle\SecurityFacade oro_security.acl.base_manager.class: Oro\Bundle\SecurityBundle\Acl\Persistence\BaseAclManager diff --git a/src/Oro/Bundle/TranslationBundle/Controller/ServiceController.php b/src/Oro/Bundle/TranslationBundle/Controller/ServiceController.php index 11fac7737df..54bfa005e12 100644 --- a/src/Oro/Bundle/TranslationBundle/Controller/ServiceController.php +++ b/src/Oro/Bundle/TranslationBundle/Controller/ServiceController.php @@ -67,6 +67,7 @@ public function downloadAction($code) if ($installed) { $this->setLanguageInstalled($code); + $this->dumpTranslations($code); $data['success'] = true; } else { $data['message'] = $this->get('translator')->trans('oro.translation.download.error'); @@ -118,4 +119,12 @@ function ($langInfo) use ($code) { // clear statistic cache $statisticProvider->clear(); } + + /** + * @param $code + */ + protected function dumpTranslations($code) + { + $this->get('oro_translation.js_dumper')->dumpTranslations([$code]); + } } diff --git a/src/Oro/Bundle/TranslationBundle/Provider/TranslationServiceProvider.php b/src/Oro/Bundle/TranslationBundle/Provider/TranslationServiceProvider.php index e904a6991aa..96c3fba7760 100644 --- a/src/Oro/Bundle/TranslationBundle/Provider/TranslationServiceProvider.php +++ b/src/Oro/Bundle/TranslationBundle/Provider/TranslationServiceProvider.php @@ -20,7 +20,11 @@ class TranslationServiceProvider /** @var AbstractAPIAdapter */ protected $adapter; - /** @var JsTranslationDumper */ + /** + * @var JsTranslationDumper + * + * @deprecated since 1.12 $jsTranslationDumper will be removed since 1.14 + */ protected $jsTranslationDumper; /** @var NullLogger */ @@ -41,6 +45,8 @@ class TranslationServiceProvider * @param TranslationLoader $translationLoader * @param DatabasePersister $databasePersister * @param string $cacheDir + * + * @deprecated since 1.12 $jsTranslationDumper argument will be removed since 1.14 */ public function __construct( AbstractAPIAdapter $adapter, @@ -163,7 +169,6 @@ public function download($pathToSave, array $projects, $locale = null, $toApply $this->apply($locale, $targetDir); $this->cleanup($targetDir); - $this->jsTranslationDumper->dumpTranslations([$locale]); } return $isExtracted && $isDownloaded; @@ -302,7 +307,6 @@ public function setLogger(LoggerInterface $logger) $this->logger = $logger; $this->adapter->setLogger($this->logger); - $this->jsTranslationDumper->setLogger($this->logger); } /** diff --git a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Provider/TranslationServiceTest.php b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Provider/TranslationServiceTest.php index f585c6acb91..1692dfb7abd 100644 --- a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Provider/TranslationServiceTest.php +++ b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Provider/TranslationServiceTest.php @@ -175,10 +175,6 @@ public function testDownload() ->method('apply') ->will($this->returnValue(['en'])); - $this->dumper->expects($this->once()) - ->method('dumpTranslations') - ->with(['en']); - $service->download($path, ['Oro'], 'en'); } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/hidden-redirect-component.js b/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/hidden-redirect-component.js index 362c3ddfe2b..a65fa9150f2 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/hidden-redirect-component.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/hidden-redirect-component.js @@ -24,6 +24,11 @@ define(function(require) { */ type: 'info', + /** + * @property {Boolean} + */ + showLoading: false, + /** * @inheritDoc */ @@ -37,8 +42,14 @@ define(function(require) { this.type = options.type; } + if (options.showLoading) { + this.showLoading = options.showLoading; + } + var self = this; this.element.on('click.' + this.cid, function(e) { + self._showLoading(); + e.preventDefault(); var pageStateView = mediator.execute('composer:retrieve', 'pageState', true); @@ -74,16 +85,19 @@ define(function(require) { saveAndRedirect: function() { var form = $('form[data-collect=true]'); var actionInput = form.find('input[name="input_action"]'); + var _this = this; $.ajax({ url: this.element.attr('href'), type: 'GET', success: function(response) { + _this._hideLoading(); actionInput.val(JSON.stringify({ redirectUrl: response.url })); form.trigger('submit'); }, error: function(xhr) { + _this._hideLoading(); Error.handle({}, xhr, {enforce: true}); } }); @@ -95,9 +109,11 @@ define(function(require) { url: this.element.attr('href'), type: 'GET', success: function(response) { + _this._hideLoading(); _this._processResponse(response.url, response.message); }, error: function(xhr) { + _this._hideLoading(); Error.handle({}, xhr, {enforce: true}); } }); @@ -158,6 +174,22 @@ define(function(require) { */ _showMessage: function(type, message) { mediator.execute('showFlashMessage', type, message); + }, + + _showLoading: function() { + if (!this.showLoading) { + return; + } + + mediator.execute('showLoading'); + }, + + _hideLoading: function() { + if (!this.showLoading) { + return; + } + + mediator.execute('hideLoading'); } }); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/tools/text-util.js b/src/Oro/Bundle/UIBundle/Resources/public/js/tools/text-util.js index 3acd077165d..96c30587ba0 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/tools/text-util.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/tools/text-util.js @@ -1,4 +1,4 @@ -define(['orotranslation/js/translator'], function(__) { +define(['underscore', 'orotranslation/js/translator'], function(_, __) { 'use strict'; // matches "A. L. Price", "In progress", "one of all" @@ -24,6 +24,9 @@ define(['orotranslation/js/translator'], function(__) { * @returns {string} */ prepareText: function(text) { + if (!_.isString(text)) { + return text; + } // disallow line breaks at start of string if there are short words at the start text = text.replace(shortWordsAtStartRegExp, function() { // can be one of two following cases @@ -73,7 +76,10 @@ define(['orotranslation/js/translator'], function(__) { * @return {string} */ abbreviate: function(text, minWordsCount) { - var words = text.split(/\W+/); + if (!_.isString(text)) { + return text; + } + var words = _.compact(text.split(/\W+/)); if (words.length < minWordsCount) { return text; } diff --git a/src/Oro/Bundle/UserBundle/Entity/AbstractUser.php b/src/Oro/Bundle/UserBundle/Entity/AbstractUser.php index 0e94e5fc4e7..6bbfcf60301 100644 --- a/src/Oro/Bundle/UserBundle/Entity/AbstractUser.php +++ b/src/Oro/Bundle/UserBundle/Entity/AbstractUser.php @@ -325,7 +325,7 @@ public function getLastLogin() * * @return AbstractUser */ - public function setLastLogin(\DateTime $time) + public function setLastLogin(\DateTime $time = null) { $this->lastLogin = $time; diff --git a/src/Oro/Bundle/UserBundle/Entity/User.php b/src/Oro/Bundle/UserBundle/Entity/User.php index 5f0c995b488..5f8f8876690 100644 --- a/src/Oro/Bundle/UserBundle/Entity/User.php +++ b/src/Oro/Bundle/UserBundle/Entity/User.php @@ -599,11 +599,11 @@ public function getCreatedAt() } /** - * @param \DateTime $createdAt + * @param \DateTime|null $createdAt * * @return User */ - public function setCreatedAt(\DateTime $createdAt) + public function setCreatedAt(\DateTime $createdAt = null) { $this->createdAt = $createdAt; @@ -621,11 +621,11 @@ public function getUpdatedAt() } /** - * @param \DateTime $updatedAt + * @param \DateTime|null $updatedAt * * @return User */ - public function setUpdatedAt(\DateTime $updatedAt) + public function setUpdatedAt(\DateTime $updatedAt = null) { $this->updatedAt = $updatedAt; diff --git a/src/Oro/Component/EntitySerializer/EntitySerializer.php b/src/Oro/Component/EntitySerializer/EntitySerializer.php index e036ab48504..b47637a67e9 100644 --- a/src/Oro/Component/EntitySerializer/EntitySerializer.php +++ b/src/Oro/Component/EntitySerializer/EntitySerializer.php @@ -347,7 +347,7 @@ protected function serializeItem($entity, $entityClass, EntityConfig $config) } $result[$field] = $value; } elseif (null !== $fieldConfig) { - $propertyPath = $fieldConfig->getPropertyPath() ?: $field; + $propertyPath = $fieldConfig->getPropertyPath($field); if (ConfigUtil::isMetadataProperty($propertyPath)) { $result[$field] = $this->fieldAccessor->getMetadataProperty( $entity, @@ -612,7 +612,7 @@ protected function getIdFieldNameIfIdOnlyRequested(EntityConfig $config, $entity return null; } - $propertyPath = $field->getPropertyPath() ?: $fieldName; + $propertyPath = $field->getPropertyPath($fieldName); if ($this->doctrineHelper->getEntityIdFieldName($entityClass) !== $propertyPath) { return null; } @@ -699,7 +699,7 @@ protected function loadRelatedItemsForSimpleEntity($entityIds, $mapping, EntityC } } } else { - $fields = $this->fieldAccessor->getFieldsToSerialize($entityClass, $config); + $fields = $this->fieldAccessor->getFieldsToSelect($entityClass, $config); foreach ($fields as $field) { $qb->addSelect(sprintf('r.%s', $field)); } diff --git a/src/Oro/Component/EntitySerializer/FieldAccessor.php b/src/Oro/Component/EntitySerializer/FieldAccessor.php index 81c59982e17..c390151b30f 100644 --- a/src/Oro/Component/EntitySerializer/FieldAccessor.php +++ b/src/Oro/Component/EntitySerializer/FieldAccessor.php @@ -76,7 +76,7 @@ public function getFields($entityClass, EntityConfig $config) } $fieldConfigs = $config->getFields(); foreach ($fieldConfigs as $field => $fieldConfig) { - if (ConfigUtil::isMetadataProperty($fieldConfig->getPropertyPath() ?: $field)) { + if (ConfigUtil::isMetadataProperty($fieldConfig->getPropertyPath($field))) { $result[] = $field; } } diff --git a/src/Oro/Component/EntitySerializer/FieldConfig.php b/src/Oro/Component/EntitySerializer/FieldConfig.php index 7920bb22a6d..e405cfafc4c 100644 --- a/src/Oro/Component/EntitySerializer/FieldConfig.php +++ b/src/Oro/Component/EntitySerializer/FieldConfig.php @@ -149,13 +149,15 @@ public function setCollapsed($collapse = true) /** * Gets the path of the field value. * + * @param string|null $defaultValue + * * @return string|null */ - public function getPropertyPath() + public function getPropertyPath($defaultValue = null) { - return array_key_exists(self::PROPERTY_PATH, $this->items) + return !empty($this->items[self::PROPERTY_PATH]) ? $this->items[self::PROPERTY_PATH] - : null; + : $defaultValue; } /** diff --git a/src/Oro/Component/EntitySerializer/Tests/Unit/FieldConfigTest.php b/src/Oro/Component/EntitySerializer/Tests/Unit/FieldConfigTest.php index 26b23b27743..6102599d42c 100644 --- a/src/Oro/Component/EntitySerializer/Tests/Unit/FieldConfigTest.php +++ b/src/Oro/Component/EntitySerializer/Tests/Unit/FieldConfigTest.php @@ -91,13 +91,16 @@ public function testPropertyPath() { $fieldConfig = new FieldConfig(); $this->assertNull($fieldConfig->getPropertyPath()); + $this->assertEquals('default', $fieldConfig->getPropertyPath('default')); $fieldConfig->setPropertyPath('test'); $this->assertEquals('test', $fieldConfig->getPropertyPath()); + $this->assertEquals('test', $fieldConfig->getPropertyPath('default')); $this->assertEquals(['property_path' => 'test'], $fieldConfig->toArray()); $fieldConfig->setPropertyPath(); $this->assertNull($fieldConfig->getPropertyPath()); + $this->assertEquals('default', $fieldConfig->getPropertyPath('default')); $this->assertEquals([], $fieldConfig->toArray()); } diff --git a/src/Oro/Component/EntitySerializer/Tests/Unit/SimpleEntitySerializerTest.php b/src/Oro/Component/EntitySerializer/Tests/Unit/SimpleEntitySerializerTest.php index 4b11e710abc..0de483645e4 100644 --- a/src/Oro/Component/EntitySerializer/Tests/Unit/SimpleEntitySerializerTest.php +++ b/src/Oro/Component/EntitySerializer/Tests/Unit/SimpleEntitySerializerTest.php @@ -664,14 +664,15 @@ public function testSimpleEntityWithPostAction() $this->setQueryExpectationAt( $conn, 1, - 'SELECT u0_.id AS id_0, p1_.name AS name_1' + 'SELECT u0_.id AS id_0, p1_.name AS name_1, p1_.id AS id_2' . ' FROM product_table p1_' . ' INNER JOIN user_table u0_ ON (p1_.owner_id = u0_.id)' . ' WHERE u0_.id = ?', [ [ 'id_0' => 1, - 'name_1' => 'product_name' + 'name_1' => 'product_name', + 'id_2' => 10 ] ], [1 => 1], @@ -1101,4 +1102,98 @@ public function testNotConfiguredRelations() $result ); } + + public function testSimpleEntityWithRenamedFields() + { + $qb = $this->em->getRepository('Test:User')->createQueryBuilder('e') + ->where('e.id = :id') + ->setParameter('id', 1); + + $conn = $this->getDriverConnectionMock($this->em); + + $this->setQueryExpectationAt( + $conn, + 0, + 'SELECT u0_.id AS id_0, u0_.name AS name_1,' + . ' c1_.name AS name_2, c1_.label AS label_3,' + . ' u0_.category_name AS category_name_4' + . ' FROM user_table u0_' + . ' LEFT JOIN category_table c1_ ON u0_.category_name = c1_.name' + . ' WHERE u0_.id = ?', + [ + [ + 'id_0' => 1, + 'name_1' => 'user_name', + 'name_2' => 'category_name', + 'label_3' => 'category_label', + 'category_name_4' => 'category_name' + ] + ], + [1 => 1], + [1 => \PDO::PARAM_INT] + ); + + $this->setQueryExpectationAt( + $conn, + 1, + 'SELECT u0_.id AS id_0, p1_.name AS name_1, p1_.id AS id_2' + . ' FROM product_table p1_' + . ' INNER JOIN user_table u0_ ON (p1_.owner_id = u0_.id)' + . ' WHERE u0_.id = ?', + [ + [ + 'id_0' => 1, + 'name_1' => 'product_name', + 'id_2' => 10 + ] + ], + [1 => 1], + [1 => \PDO::PARAM_INT] + ); + + $result = $this->serializer->serialize( + $qb, + [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'id' => null, + 'renamedName' => [ + 'property_path' => 'name' + ], + 'category' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'renamedLabel' => [ + 'property_path' => 'label' + ] + ], + ], + 'products' => [ + 'exclusion_policy' => 'all', + 'fields' => [ + 'renamedName' => [ + 'property_path' => 'name' + ] + ], + ], + ] + ] + ); + + $this->assertArrayEquals( + [ + [ + 'id' => 1, + 'renamedName' => 'user_name', + 'category' => [ + 'renamedLabel' => 'category_label' + ], + 'products' => [ + ['renamedName' => 'product_name'] + ] + ] + ], + $result + ); + } }