diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6b24d8..1dd35a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,6 @@ jobs: matrix: php: - - '7.1' - - '7.2' - - '7.3' - '7.4' - '8.0' - '8.1' @@ -35,17 +32,13 @@ jobs: dependency: - 'highest' include: - - { php: '7.1', symfony: '3.*', dependency: 'lowest' } + - { php: '7.4', symfony: '3.*', dependency: 'lowest' } exclude: - { php: '8.0', symfony: '3.*' } - { php: '8.1', symfony: '3.*' } - { php: '8.2', symfony: '3.*' } - { php: '8.3', symfony: '3.*' } - { php: '8.4', symfony: '3.*' } - - { php: '7.1', symfony: '5.*' } - - { php: '7.1', symfony: '6.*' } - - { php: '7.2', symfony: '6.*' } - - { php: '7.3', symfony: '6.*' } - { php: '7.4', symfony: '6.*' } - { php: '8.0', symfony: '6.*' } @@ -60,12 +53,7 @@ jobs: tools: flex - name: Ignore specific Composer audit advisory - run: | - if [[ "${{ matrix.php }}" == "7.1" ]]; then - echo "COMPOSER_AUDIT_BLOCK_INSECURE=0" >> $GITHUB_ENV - else - composer config --global audit.ignore "PKSA-w2tw-kmfg-rt9s" - fi + run: composer config --global audit.ignore "PKSA-w2tw-kmfg-rt9s" - name: Install dependencies uses: ramsey/composer-install@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f3720..ad5f877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.5.0 +### Changed +- `DateNormalizer::mapToEntity()` rejects `null` up front instead of passing it to + `DateTime::createFromFormat()`, resolving a PHP 8.1 deprecation. `null` still raises + `InvalidDataException`, so `catch` blocks are unaffected, but the message changes from + `Provided date format is invalid` to `Date must be provided` — the old text blamed the + configured format for what is really a missing input. Consumers that match on + `getMessage()` rather than the exception type, such as API error-mapping layers, need to + account for the new string. +- Narrowed the `phpunit/phpunit` development requirement to `^9.3` — the version that introduced + the `` configuration element used by `phpunit.xml.dist`. +- Replaced leading-backslash class references with `use` statements throughout the library — + global classes (`ArrayIterator`, `ArrayObject`, `DateTime`, `DateTimeZone`, `Exception` and + the SPL exceptions) and fully qualified `Paysera\...` references in docblocks. No behaviour + change. +- Replaced long array syntax (`array(...)`) with short syntax (`[...]`) throughout the library. + No behaviour change. + +### Removed +- Dropped support for PHP 7.1, 7.2 and 7.3. Minimum supported version is now PHP 7.4. Projects + still on those versions resolve to 3.4.x and are unaffected. + +### Fixed +- `Result::getIterator()` is marked `#[\ReturnTypeWillChange]`, silencing the PHP 8.1 tentative + return type deprecation without changing the signature. The native `\Traversable` return type + is deferred to 4.0.0, where it will be batched with the other type additions. +- `CamelCaseToSnakeCaseConverter::convert()` no longer passes `null` to `preg_replace()`, + resolving a PHP 8.1 deprecation. Passing `null` still returns an empty string as before. +- `FollowUpFilter` no longer redeclares `$offset` without an initialiser. The shadowing + declaration gave it a `null` default where `Filter` declares `0`; it now inherits the parent + default. Instances built through the constructor were always assigned an offset there and are + unaffected. +- `Result::$items` now defaults to an empty array, so iterating a `Result` whose items were never + set no longer raises a `TypeError` on PHP 8 (an `InvalidArgumentException` on PHP 7.4) and + `getItems()` honours its documented `@return mixed[]`. The default lives on the property + declaration rather than in the constructor, so it also applies to instances built without one — + `ReflectionClass::newInstanceWithoutConstructor()`, and the ORM hydration and mocking that build + on it. + ## 3.4.0 ### Added - PHP 8.4 support, removed implicitly nullable parameter declarations. diff --git a/composer.json b/composer.json index e54890a..624ac9b 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,10 @@ } }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.3" }, "require": { - "php": "^7.1 || ^8.0", + "php": "^7.4 || ^8.0", "symfony/property-access": "^3.0 || ^4.0 || ^5.0 || ^6.0", "symfony/validator": "^3.0 || ^4.0 || ^5.0 || ^6.0", "willdurand/jsonp-callback-validator": "^1.0" diff --git a/src/Converter/CamelCaseToSnakeCaseConverter.php b/src/Converter/CamelCaseToSnakeCaseConverter.php index 0cf1422..78338df 100644 --- a/src/Converter/CamelCaseToSnakeCaseConverter.php +++ b/src/Converter/CamelCaseToSnakeCaseConverter.php @@ -13,7 +13,7 @@ public function convert($path) preg_replace( '/[A-Z]/u', '_$0', - $path + $path ?? '' ) ), '_' diff --git a/src/Encoding/Jsonp.php b/src/Encoding/Jsonp.php index 4b125e0..b356ebe 100644 --- a/src/Encoding/Jsonp.php +++ b/src/Encoding/Jsonp.php @@ -7,7 +7,6 @@ class Jsonp implements EncoderInterface { - protected $jsonEncoder; protected $callbackValidator; @@ -57,10 +56,10 @@ public function encode($data) $this->jsonEncoder->decode($this->parameter); } catch (EncodingException $exception) { $this->parameter = null; - $json = $this->jsonEncoder->encode(array( + $json = $this->jsonEncoder->encode([ 'error' => 'invalid_parameters', 'error_description' => 'Passed parameter must be valid JSON string', - )); + ]); } } diff --git a/src/Entity/FollowUpFilter.php b/src/Entity/FollowUpFilter.php index c7a7647..18ad7f7 100644 --- a/src/Entity/FollowUpFilter.php +++ b/src/Entity/FollowUpFilter.php @@ -4,11 +4,6 @@ class FollowUpFilter extends Filter { - /** - * @var int - */ - protected $offset; - /** * @var int */ diff --git a/src/Entity/Result.php b/src/Entity/Result.php index ea7f3e7..d077fc8 100644 --- a/src/Entity/Result.php +++ b/src/Entity/Result.php @@ -2,7 +2,13 @@ namespace Paysera\Component\Serializer\Entity; -class Result implements \IteratorAggregate, ResultInterface +use ArrayIterator; +use BadMethodCallException; +use IteratorAggregate; +use ReturnTypeWillChange; +use Traversable; + +class Result implements IteratorAggregate, ResultInterface { /** * @var int @@ -37,8 +43,7 @@ class Result implements \IteratorAggregate, ResultInterface /** * @var mixed[] */ - protected $items; - + protected $items = []; public function __construct(?Filter $filter = null) { @@ -201,14 +206,14 @@ public function addItem($item) /** * Try to calculate total result count, in case all results are fetched. * - * @param $resultCount - * @return null - * @throws \BadMethodCallException + * @param int $resultCount + * @return int|null the calculated total count, or null when it cannot be determined + * @throws BadMethodCallException */ public function calculateTotalCount($resultCount) { if (!$this->getFilter()) { - throw new \BadMethodCallException('filter must be set before calling this method'); + throw new BadMethodCallException('filter must be set before calling this method'); } if ( @@ -226,10 +231,11 @@ public function calculateTotalCount($resultCount) /** * Retrieve an external iterator * - * @return \Traversable + * @return Traversable */ + #[ReturnTypeWillChange] public function getIterator() { - return new \ArrayIterator($this->items); + return new ArrayIterator($this->items); } } diff --git a/src/Entity/ResultInterface.php b/src/Entity/ResultInterface.php index 3afbc99..1293c38 100644 --- a/src/Entity/ResultInterface.php +++ b/src/Entity/ResultInterface.php @@ -2,7 +2,9 @@ namespace Paysera\Component\Serializer\Entity; -interface ResultInterface extends \Traversable +use Traversable; + +interface ResultInterface extends Traversable { /** * Gets totalCount diff --git a/src/Exception/EncodingException.php b/src/Exception/EncodingException.php index e0f0d92..e8a5cf1 100644 --- a/src/Exception/EncodingException.php +++ b/src/Exception/EncodingException.php @@ -2,6 +2,8 @@ namespace Paysera\Component\Serializer\Exception; -class EncodingException extends \Exception +use Exception; + +class EncodingException extends Exception { } diff --git a/src/Factory/ContextAwareNormalizerFactory.php b/src/Factory/ContextAwareNormalizerFactory.php index 67bb4f1..9459e5d 100644 --- a/src/Factory/ContextAwareNormalizerFactory.php +++ b/src/Factory/ContextAwareNormalizerFactory.php @@ -4,21 +4,20 @@ use Paysera\Component\Serializer\Filter\FieldsFilter; use Paysera\Component\Serializer\Filter\FieldsParser; +use Paysera\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Paysera\Component\Serializer\Normalizer\DenormalizerInterface; use Paysera\Component\Serializer\Normalizer\DistributedNormalizer; -use Paysera\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Paysera\Component\Serializer\Normalizer\NormalizerInterface; class ContextAwareNormalizerFactory { - /** - * @var \Paysera\Component\Serializer\Filter\FieldsFilter + * @var FieldsFilter */ protected $fieldsFilter; /** - * @var \Paysera\Component\Serializer\Filter\FieldsParser + * @var FieldsParser */ protected $fieldsParser; @@ -44,4 +43,4 @@ public function create($normalizer) $normalizer ); } -} +} diff --git a/src/Factory/ResponseMapperFactory.php b/src/Factory/ResponseMapperFactory.php index d9e863f..83933c0 100644 --- a/src/Factory/ResponseMapperFactory.php +++ b/src/Factory/ResponseMapperFactory.php @@ -3,6 +3,7 @@ namespace Paysera\Component\Serializer\Factory; use Paysera\Component\Serializer\Normalizer\NormalizerInterface; +use RuntimeException; class ResponseMapperFactory implements ResponseMapperFactoryInterface { @@ -22,7 +23,7 @@ class ResponseMapperFactory implements ResponseMapperFactoryInterface public function __construct(NormalizerInterface $defaultMapper) { $this->defaultMapper = $defaultMapper; - $this->mappers = array(); + $this->mappers = []; } /** @@ -40,14 +41,14 @@ public function addMapper($key, NormalizerInterface $mapper) /** * @param array $options * - * @throws \RuntimeException + * @throws RuntimeException * @return NormalizerInterface */ public function createResponseMapper(array $options) { $key = isset($options[self::MAPPER_OPTION]) ? $options[self::MAPPER_OPTION] : null; if ($key !== null && !isset($this->mappers[$key])) { - throw new \RuntimeException('Wrong mapper key specified: ' . $key); + throw new RuntimeException('Wrong mapper key specified: ' . $key); } if ($key === null) { foreach ($options as $optionKey => $value) { @@ -58,4 +59,4 @@ public function createResponseMapper(array $options) } return $key !== null ? $this->mappers[$key] : $this->defaultMapper; } -} +} diff --git a/src/Filter/FieldsConfig.php b/src/Filter/FieldsConfig.php index 7b9dff9..d256c3e 100644 --- a/src/Filter/FieldsConfig.php +++ b/src/Filter/FieldsConfig.php @@ -55,7 +55,7 @@ public function getFieldExtensions($fieldName) } return $extensions; } else { - return $this->defaultsIncluded ? array('*') : array(); + return $this->defaultsIncluded ? ['*'] : []; } } @@ -66,4 +66,4 @@ public function areDefaultsIncluded() { return $this->defaultsIncluded; } -} +} diff --git a/src/Filter/FieldsFilter.php b/src/Filter/FieldsFilter.php index 0423384..59de91a 100644 --- a/src/Filter/FieldsFilter.php +++ b/src/Filter/FieldsFilter.php @@ -2,6 +2,8 @@ namespace Paysera\Component\Serializer\Filter; +use ArrayObject; + class FieldsFilter { /** @@ -33,7 +35,7 @@ public function filter($data, ?array $fields = null, array $scope = []) if ($this->isAssociativeArray($data)) { $result = $this->filterByConfig($data, $fieldsConfig); if (is_array($result) && count($result) === 0) { - $result = new \ArrayObject(); + $result = new ArrayObject(); } return $result; } else { @@ -52,7 +54,7 @@ public function filter($data, ?array $fields = null, array $scope = []) */ protected function filterByConfig($data, FieldsConfig $fieldsConfig) { - $result = array(); + $result = []; foreach ($data as $fieldName => $value) { if ($fieldsConfig->isIncluded($fieldName)) { if (is_array($value)) { diff --git a/src/Filter/FieldsParser.php b/src/Filter/FieldsParser.php index 885c95e..8e5b029 100644 --- a/src/Filter/FieldsParser.php +++ b/src/Filter/FieldsParser.php @@ -2,9 +2,10 @@ namespace Paysera\Component\Serializer\Filter; +use InvalidArgumentException; + class FieldsParser { - /** * @param null|array $fields * @param array $scope @@ -23,7 +24,7 @@ public function parseFields(?array $fields = null, array $scope = []) /** * @param null|array $fields * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * @return FieldsConfig */ public function parseUnscopedFields(?array $fields = null) @@ -33,15 +34,15 @@ public function parseUnscopedFields(?array $fields = null) } $defaultsIncluded = false; - $includedFields = array(); - $fieldExtensions = array(); + $includedFields = []; + $fieldExtensions = []; foreach ($fields as $fieldDefinition) { // todo: take curly braces? see commented test-case for possible usage foreach (explode(',', $fieldDefinition) as $field) { $list = explode('.', $field, 2); if (isset($list[1]) && $list[1] === '') { - throw new \InvalidArgumentException('Invalid field provided, field cannot end with a dot'); + throw new InvalidArgumentException('Invalid field provided, field cannot end with a dot'); } $name = $list[0]; $extension = isset($list[1]) ? $list[1] : null; @@ -63,6 +64,6 @@ public function parseUnscopedFields(?array $fields = null) */ protected function createWithDefaultsIncluded() { - return new FieldsConfig(true, array(), array()); + return new FieldsConfig(true, [], []); } } diff --git a/src/Normalizer/ArrayNormalizer.php b/src/Normalizer/ArrayNormalizer.php index 244fa72..500b0e2 100644 --- a/src/Normalizer/ArrayNormalizer.php +++ b/src/Normalizer/ArrayNormalizer.php @@ -3,6 +3,7 @@ namespace Paysera\Component\Serializer\Normalizer; use Paysera\Component\Serializer\Entity\NormalizationContextInterface; +use Paysera\Component\Serializer\Exception\InvalidDataException; class ArrayNormalizer implements DenormalizerInterface, ContextAwareNormalizerInterface { @@ -28,11 +29,11 @@ public function __construct($innerMapper) * * @return mixed * - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ public function mapToEntity($data) { - $result = array(); + $result = []; if ($data !== null) { foreach ($data as $innerElement) { $result[] = $this->innerMapper->mapToEntity($innerElement); @@ -43,7 +44,7 @@ public function mapToEntity($data) public function mapFromEntity($entity, ?NormalizationContextInterface $context = null) { - $result = array(); + $result = []; if ($entity !== null) { foreach ($entity as $innerElement) { $result[] = $this->innerMapper->mapFromEntity($innerElement, $context); diff --git a/src/Normalizer/AssociativeArrayNormalizer.php b/src/Normalizer/AssociativeArrayNormalizer.php index 52c6764..3e25db0 100644 --- a/src/Normalizer/AssociativeArrayNormalizer.php +++ b/src/Normalizer/AssociativeArrayNormalizer.php @@ -2,6 +2,8 @@ namespace Paysera\Component\Serializer\Normalizer; +use ArrayObject; + class AssociativeArrayNormalizer implements DenormalizerInterface, NormalizerInterface { /** @@ -28,7 +30,7 @@ public function __construct($innerMapper) */ public function mapFromEntity($entity) { - $result = new \ArrayObject(); + $result = new ArrayObject(); if ($entity !== null) { foreach ($entity as $key => $innerElement) { $result[$key] = $this->innerMapper->mapFromEntity($innerElement); @@ -46,7 +48,7 @@ public function mapFromEntity($entity) */ public function mapToEntity($data) { - $result = array(); + $result = []; if ($data !== null) { foreach ($data as $key => $innerElement) { $result[$key] = $this->innerMapper->mapToEntity($innerElement); diff --git a/src/Normalizer/BaseDenormalizer.php b/src/Normalizer/BaseDenormalizer.php index bdb67ac..d4f8ebf 100644 --- a/src/Normalizer/BaseDenormalizer.php +++ b/src/Normalizer/BaseDenormalizer.php @@ -63,7 +63,7 @@ protected function checkRequiredKeys($data, $requiredKeys) * * @param $data * @param $keys - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ protected function checkOnlyOneKeyExists($data, $keys) { @@ -81,4 +81,4 @@ protected function checkOnlyOneKeyExists($data, $keys) } } } -} +} diff --git a/src/Normalizer/DateNormalizer.php b/src/Normalizer/DateNormalizer.php index cd86fac..b148a66 100644 --- a/src/Normalizer/DateNormalizer.php +++ b/src/Normalizer/DateNormalizer.php @@ -2,6 +2,8 @@ namespace Paysera\Component\Serializer\Normalizer; +use DateTime; +use DateTimeZone; use Paysera\Component\Serializer\Exception\InvalidDataException; class DateNormalizer extends BaseDenormalizer implements NormalizerInterface @@ -12,7 +14,7 @@ class DateNormalizer extends BaseDenormalizer implements NormalizerInterface protected $format; /** - * @var \DateTimeZone + * @var DateTimeZone */ protected $remoteTimezone; @@ -25,11 +27,15 @@ public function __construct($format, $remoteTimezone = null) /** * @param string $data * @throws InvalidDataException - * @return \DateTime + * @return DateTime */ public function mapToEntity($data) { - $date = \DateTime::createFromFormat( + if ($data === null) { + throw new InvalidDataException('Date must be provided'); + } + + $date = DateTime::createFromFormat( $this->format, $data, $this->remoteTimezone @@ -50,7 +56,7 @@ public function mapToEntity($data) } /** - * @param \DateTime $entity + * @param DateTime $entity * @return string */ public function mapFromEntity($entity) @@ -67,6 +73,6 @@ public function mapFromEntity($entity) protected function getLocalTimezone() { - return new \DateTimeZone(date_default_timezone_get()); + return new DateTimeZone(date_default_timezone_get()); } } diff --git a/src/Normalizer/DenormalizerInterface.php b/src/Normalizer/DenormalizerInterface.php index 202a869..23329d8 100644 --- a/src/Normalizer/DenormalizerInterface.php +++ b/src/Normalizer/DenormalizerInterface.php @@ -2,6 +2,8 @@ namespace Paysera\Component\Serializer\Normalizer; +use Paysera\Component\Serializer\Exception\InvalidDataException; + interface DenormalizerInterface { /** @@ -11,7 +13,7 @@ interface DenormalizerInterface * * @return mixed * - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ public function mapToEntity($data); } diff --git a/src/Normalizer/DistributedNormalizer.php b/src/Normalizer/DistributedNormalizer.php index 3ace9a2..4ec04c0 100644 --- a/src/Normalizer/DistributedNormalizer.php +++ b/src/Normalizer/DistributedNormalizer.php @@ -4,6 +4,7 @@ use Paysera\Component\Serializer\Accessor\FieldAccessorInterface; use Paysera\Component\Serializer\Entity\NormalizationContextInterface; +use Paysera\Component\Serializer\Exception\InvalidDataException; use Paysera\Component\Serializer\Factory\ContextAwareNormalizerFactory; use Paysera\Component\Serializer\Filter\FieldsFilter; use Paysera\Component\Serializer\Filter\FieldsParser; @@ -16,12 +17,12 @@ class DistributedNormalizer implements DenormalizerInterface, ContextAwareNormal protected $factory; /** - * @var \Paysera\Component\Serializer\Filter\FieldsFilter + * @var FieldsFilter */ protected $fieldsFilter; /** - * @var \Paysera\Component\Serializer\Filter\FieldsParser + * @var FieldsParser */ protected $fieldsParser; @@ -33,23 +34,23 @@ class DistributedNormalizer implements DenormalizerInterface, ContextAwareNormal /** * @var FieldAccessorInterface[] */ - protected $fieldAccessors = array(); + protected $fieldAccessors = []; /** * @var array of boolean */ - protected $fieldDefault = array(); + protected $fieldDefault = []; /** * @var DenormalizerInterface[]|NormalizerInterface[] */ - protected $fieldNormalizers = array(); + protected $fieldNormalizers = []; /** - * @param \Paysera\Component\Serializer\Factory\ContextAwareNormalizerFactory $factory - * @param \Paysera\Component\Serializer\Filter\FieldsParser $fieldsParser - * @param \Paysera\Component\Serializer\Filter\FieldsFilter $fieldsFilter - * @param DenormalizerInterface|NormalizerInterface $normalizer + * @param ContextAwareNormalizerFactory $factory + * @param FieldsParser $fieldsParser + * @param FieldsFilter $fieldsFilter + * @param DenormalizerInterface|NormalizerInterface $normalizer */ public function __construct( ContextAwareNormalizerFactory $factory, @@ -97,11 +98,11 @@ public function addAdditionalField($fieldName, FieldAccessorInterface $fieldAcce * * @return mixed * - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ public function mapToEntity($data) { - $additional = array(); + $additional = []; if (is_array($data)) { foreach ($data as $key => $value) { if ( @@ -141,7 +142,7 @@ public function mapFromEntity($entity, ?NormalizationContextInterface $context = } $fields = $context !== null ? $context->getFields() : null; - $scope = $context !== null ? $context->getScope() : array(); + $scope = $context !== null ? $context->getScope() : []; $data = $this->fieldsFilter->filter($data, $fields, $scope); $fieldsConfig = $this->fieldsParser->parseFields($fields, $scope); diff --git a/src/Normalizer/FilterNormalizer.php b/src/Normalizer/FilterNormalizer.php index a49abab..87e2b98 100644 --- a/src/Normalizer/FilterNormalizer.php +++ b/src/Normalizer/FilterNormalizer.php @@ -11,7 +11,7 @@ class FilterNormalizer extends BaseDenormalizer implements NormalizerInterface protected $maxLimit; protected $orderByFields; - public function __construct($orderByFields = array(), $defaultLimit = 20, $maxLimit = 200) + public function __construct($orderByFields = [], $defaultLimit = 20, $maxLimit = 200) { $this->defaultLimit = $defaultLimit; $this->maxLimit = $maxLimit; @@ -25,7 +25,7 @@ public function __construct($orderByFields = array(), $defaultLimit = 20, $maxLi * * @return mixed * - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ public function mapToEntity($data) { @@ -41,7 +41,7 @@ public function mapToEntity($data) */ public function mapFromEntity($entity) { - $data = array(); + $data = []; if ($entity->getLimit() !== null) { $data['limit'] = $entity->getLimit(); } @@ -124,7 +124,7 @@ protected function mapBaseKeys($data, Filter $filter) throw new InvalidDataException('order_direction is unsupported for this method'); } $orderDirection = strtoupper($orderDirection); - if (!in_array($orderDirection, array('ASC', 'DESC'))) { + if (!in_array($orderDirection, ['ASC', 'DESC'])) { throw new InvalidDataException('Invalid order_direction value'); } $filter->setOrderAsc($orderDirection === 'ASC'); diff --git a/src/Normalizer/PlainNormalizer.php b/src/Normalizer/PlainNormalizer.php index 7a4b288..dbefbae 100644 --- a/src/Normalizer/PlainNormalizer.php +++ b/src/Normalizer/PlainNormalizer.php @@ -2,6 +2,8 @@ namespace Paysera\Component\Serializer\Normalizer; +use Paysera\Component\Serializer\Exception\InvalidDataException; + class PlainNormalizer implements NormalizerInterface, DenormalizerInterface { /** @@ -23,7 +25,7 @@ public function mapFromEntity($entity) * * @return mixed * - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ public function mapToEntity($data) { diff --git a/src/Normalizer/ResultMetadataNormalizer.php b/src/Normalizer/ResultMetadataNormalizer.php index 65ff8f9..1791809 100644 --- a/src/Normalizer/ResultMetadataNormalizer.php +++ b/src/Normalizer/ResultMetadataNormalizer.php @@ -6,7 +6,6 @@ class ResultMetadataNormalizer implements NormalizerInterface { - /** * Maps some structure to raw data. Usually entity object to array * @@ -18,10 +17,10 @@ public function mapFromEntity($result) { $filter = $result->getFilter(); - $data = array( + $data = [ 'total' => $result->getTotalCount(), 'limit' => $filter ? $filter->getLimit() : null, - ); + ]; if ($result->getAfter() !== null) { $data['cursors']['after'] = $result->getAfter(); diff --git a/src/Normalizer/ResultNormalizer.php b/src/Normalizer/ResultNormalizer.php index 729233b..1f62227 100644 --- a/src/Normalizer/ResultNormalizer.php +++ b/src/Normalizer/ResultNormalizer.php @@ -5,6 +5,7 @@ use Paysera\Component\Serializer\Entity\Filter; use Paysera\Component\Serializer\Entity\NormalizationContextInterface; use Paysera\Component\Serializer\Entity\Result; +use Paysera\Component\Serializer\Exception\InvalidDataException; class ResultNormalizer implements ContextAwareNormalizerInterface, DenormalizerInterface { @@ -58,13 +59,13 @@ public function setMetadataNormalizer($metadataNormalizer) */ public function mapFromEntity($entity, ?NormalizationContextInterface $context = null) { - return array( + return [ $this->itemsKey => $this->mapItemsFromEntity( $entity->getItems(), $context !== null ? $context->createScopedContext($this->itemsKey) : null ), '_metadata' => $this->mapMetadataFromEntity($entity), - ); + ]; } /** @@ -74,7 +75,7 @@ public function mapFromEntity($entity, ?NormalizationContextInterface $context = * * @return mixed * - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ public function mapToEntity($data) { diff --git a/src/Normalizer/TransformerDenormalizer.php b/src/Normalizer/TransformerDenormalizer.php index 9e312b1..d0806fe 100644 --- a/src/Normalizer/TransformerDenormalizer.php +++ b/src/Normalizer/TransformerDenormalizer.php @@ -2,6 +2,7 @@ namespace Paysera\Component\Serializer\Normalizer; +use Paysera\Component\Serializer\Exception\InvalidDataException; use Paysera\Component\Serializer\Transformer\TransformerInterface; class TransformerDenormalizer implements DenormalizerInterface @@ -33,7 +34,7 @@ public function __construct(TransformerInterface $transformer, DenormalizerInter * * @return mixed * - * @throws \Paysera\Component\Serializer\Exception\InvalidDataException + * @throws InvalidDataException */ public function mapToEntity($data) { diff --git a/src/Normalizer/ViolationNormalizer.php b/src/Normalizer/ViolationNormalizer.php index 9c5d917..780b685 100644 --- a/src/Normalizer/ViolationNormalizer.php +++ b/src/Normalizer/ViolationNormalizer.php @@ -37,7 +37,7 @@ public function mapToEntity($data) */ public function mapFromEntity($entity) { - $data = array(); + $data = []; if ($entity->getCode() !== null) { $data['code'] = $entity->getCode(); } diff --git a/src/Validation/PropertiesAwareValidator.php b/src/Validation/PropertiesAwareValidator.php index ba9656c..31a30ef 100644 --- a/src/Validation/PropertiesAwareValidator.php +++ b/src/Validation/PropertiesAwareValidator.php @@ -53,8 +53,8 @@ public function validate($entity, $groups = null) } if ($violationList->count() > 0) { - $properties = array(); - $violations = array(); + $properties = []; + $violations = []; foreach ($violationList as $violation) { $path = $violation->getPropertyPath(); diff --git a/tests/Entity/FilterTest.php b/tests/Entity/FilterTest.php new file mode 100644 index 0000000..296332d --- /dev/null +++ b/tests/Entity/FilterTest.php @@ -0,0 +1,47 @@ +assertSame(0, (new Filter())->getOffset()); + } + + public function testOffsetDefaultsToZeroForSubclassNotCallingParentConstructor() + { + $this->assertSame(0, (new OwnConstructorFilter('done'))->getOffset()); + } + + public function testOffsetDefaultsToZeroWhenConstructorIsBypassed() + { + $filter = (new ReflectionClass(Filter::class))->newInstanceWithoutConstructor(); + + $this->assertSame(0, $filter->getOffset()); + } + + public function testGetOffsetReturnsNullWhenCursorIsUsed() + { + $this->assertNull((new Filter())->setAfter('cursor')->getOffset()); + $this->assertNull((new Filter())->setBefore('cursor')->getOffset()); + } + + public function testFollowUpFilterInheritsOffsetDefault() + { + $filter = (new ReflectionClass(FollowUpFilter::class))->newInstanceWithoutConstructor(); + + $this->assertSame(0, $filter->getOffset()); + } + + public function testFollowUpFilterKeepsConstructorOffset() + { + $this->assertSame(10, (new FollowUpFilter(5, 10))->getOffset()); + } +} diff --git a/tests/Entity/ResultTest.php b/tests/Entity/ResultTest.php new file mode 100644 index 0000000..c30d97d --- /dev/null +++ b/tests/Entity/ResultTest.php @@ -0,0 +1,105 @@ +assertSame([], iterator_to_array($result)); + } + + public function testGetItemsReturnsArrayWhenItemsNotSet() + { + $result = new Result(); + + $this->assertSame([], $result->getItems()); + } + + public function testIterateResultWithItems() + { + $result = (new Result())->setItems([1, 2, 3]); + + $this->assertSame([1, 2, 3], iterator_to_array($result)); + } + + public function testAddItemWithoutSettingItemsFirst() + { + $result = new Result(); + $result->addItem('a'); + + $this->assertSame(['a'], $result->getItems()); + } + + public function testTotalCountDefaultAppliesWhenConstructorIsBypassed() + { + $result = (new ReflectionClass(Result::class))->newInstanceWithoutConstructor(); + + $this->assertSame(0, $result->getTotalCount()); + } + + /** + * Reflection-based instantiation is how ORM hydration, reflection serializers and + * PHPUnit's disableOriginalConstructor() build objects. Pins the defaults to the + * property declarations: a constructor assignment would not cover this path. + */ + public function testItemsDefaultAppliesWhenConstructorIsBypassed() + { + $result = (new ReflectionClass(Result::class))->newInstanceWithoutConstructor(); + + $this->assertSame([], $result->getItems()); + $this->assertSame([], iterator_to_array($result)); + } + + /** + * Filter carries its offset default on the property declaration, so a descendant + * that declares its own constructor without calling parent::__construct() still + * reports 0 rather than null — which is what keeps this calculation from silently + * skipping and reporting a total of 0 for a non-empty result set. + */ + public function testCalculateTotalCountForFilterSubclassNotCallingParentConstructor() + { + $result = (new Result(new OwnConstructorFilter()))->setItems([1, 2]); + + $this->assertSame(2, $result->calculateTotalCount(2)); + $this->assertSame(2, $result->getTotalCount()); + } + + /** + * The PHP 8.1 tentative return type notice is emitted when the class is + * declared, not when getIterator() is called, so it cannot be caught with an + * error handler from inside a test. Assert the declaration instead: either a + * native return type or the attribute keeps IteratorAggregate quiet. + */ + public function testGetIteratorSuppressesTentativeReturnTypeDeprecation() + { + $method = new ReflectionMethod(Result::class, 'getIterator'); + + if ($method->hasReturnType()) { + $this->assertSame('Traversable', (string)$method->getReturnType()); + return; + } + + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Attributes require PHP 8.0; the notice only exists on PHP 8.1+.'); + } + + $attributes = array_map( + function ($attribute) { + return $attribute->getName(); + }, + $method->getAttributes() + ); + + $this->assertContains(ReturnTypeWillChange::class, $attributes); + } +} diff --git a/tests/Filter/FieldsFilterTest.php b/tests/Filter/FieldsFilterTest.php index 2526e6a..ea57665 100644 --- a/tests/Filter/FieldsFilterTest.php +++ b/tests/Filter/FieldsFilterTest.php @@ -2,6 +2,7 @@ namespace Paysera\Component\Serializer\Tests\Filter; +use ArrayObject; use Paysera\Component\Serializer\Filter\FieldsFilter; use Paysera\Component\Serializer\Filter\FieldsParser; use PHPUnit\Framework\TestCase; @@ -51,258 +52,258 @@ public function testFilterWithScope($data, $fields, $scope, $result) public function filterProvider() { - $simple = array( + $simple = [ 'key1' => 'value1', 'key2' => 'value2', - ); - $complex = array( + ]; + $complex = [ 'key1' => 'value1', 'key2' => 'value2', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1', 'key2' => 'value2'), - ); + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1', 'key2' => 'value2'], + ]; - return array( - 'Matches everything if * provided' => array( + return [ + 'Matches everything if * provided' => [ 'data' => $simple, - 'fields' => array('*'), + 'fields' => ['*'], 'result' => $simple, - ), - 'Filters first level data' => array( + ], + 'Filters first level data' => [ 'data' => $complex, - 'fields' => array('key1', 'key4', 'key3'), - 'result' => array( + 'fields' => ['key1', 'key4', 'key3'], + 'result' => [ 'key1' => 'value1', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1', 'key2' => 'value2'), - ), - ), - 'Ignores additional fields if * provided' => array( + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1', 'key2' => 'value2'], + ], + ], + 'Ignores additional fields if * provided' => [ 'data' => $complex, - 'fields' => array('key1', '*', 'key2'), + 'fields' => ['key1', '*', 'key2'], 'result' => $complex, - ), - 'Filters second level data' => array( + ], + 'Filters second level data' => [ 'data' => $complex, - 'fields' => array('key1', 'key3', 'key4.key1'), - 'result' => array( + 'fields' => ['key1', 'key3', 'key4.key1'], + 'result' => [ 'key1' => 'value1', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1'), - ), - ), - 'Takes subtree if item provided' => array( + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1'], + ], + ], + 'Takes subtree if item provided' => [ 'data' => $complex, - 'fields' => array('key1', 'key3', 'key4'), - 'result' => array( + 'fields' => ['key1', 'key3', 'key4'], + 'result' => [ 'key1' => 'value1', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1', 'key2' => 'value2'), - ), - ), - 'Takes subtree if item with * provided' => array( + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1', 'key2' => 'value2'], + ], + ], + 'Takes subtree if item with * provided' => [ 'data' => $complex, - 'fields' => array('key1', 'key3', 'key4.*', 'key.key2'), - 'result' => array( + 'fields' => ['key1', 'key3', 'key4.*', 'key.key2'], + 'result' => [ 'key1' => 'value1', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1', 'key2' => 'value2'), - ), - ), - 'Ignores additional fields' => array( + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1', 'key2' => 'value2'], + ], + ], + 'Ignores additional fields' => [ 'data' => $complex, - 'fields' => array('key1', 'key3', 'key4.*', 'key.key2', 'newkey', 'newkey.a', 'newkey.*', 'key4.new'), - 'result' => array( + 'fields' => ['key1', 'key3', 'key4.*', 'key.key2', 'newkey', 'newkey.a', 'newkey.*', 'key4.new'], + 'result' => [ 'key1' => 'value1', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1', 'key2' => 'value2'), - ), - ), - 'Do not take with key 0' => array( - 'data' => array('payments' => array( - '0' => array('id' => 123, 'description' => 'abc1'), - 'asd' => array('id' => 124, 'description' => 'abc2'), - 'b' => array('id' => 125, 'description' => 'abc3'), - )), - 'fields' => array('payments.asd'), - 'result' => array('payments' => array( - 'asd' => array('id' => 124, 'description' => 'abc2'), - )), - ), - 'Takes numeric keys' => array( - 'data' => array('payments' => array( - '1' => array('id' => 123, 'description' => 'abc1'), - 'asd' => array('id' => 124, 'description' => 'abc2'), - 'b' => array('id' => 125, 'description' => 'abc3'), - )), - 'fields' => array('payments.1'), - 'result' => array('payments' => array( - '1' => array('id' => 123, 'description' => 'abc1'), - )), - ), - 'Filters for array items' => array( - 'data' => array('payments' => array( - array('id' => 123, 'description' => 'abc1'), - array('id' => 124, 'description' => 'abc2'), - array('id' => 125, 'description' => 'abc3'), - )), - 'fields' => array('payments.id'), - 'result' => array('payments' => array( - array('id' => 123), - array('id' => 124), - array('id' => 125), - )), - ), - 'Filters for array items at top level' => array( - 'data' => array( - array('id' => 123, 'description' => 'abc1'), - array('id' => 124, 'description' => 'abc2'), - array('id' => 125, 'description' => 'abc3'), - ), - 'fields' => array('id'), - 'result' => array( - array('id' => 123), - array('id' => 124), - array('id' => 125), - ), - ), - 'Correctly gets associative arrays' => array( - 'data' => array('payments' => array( - array('id' => 123, 'description' => 'abc1'), - array('id' => 124, 'description' => 'abc2'), - 5 => array('id' => 125, 'description' => 'abc3'), - )), - 'fields' => array('payments.id'), - 'result' => array('payments' => new \ArrayObject()), - ), - 'Takes all fields if wildcard on parent specified' => array( - 'data' => array('a1' => array('a2' => array('a3' => array('a4' => 'value1', 'a5' => 'value2')))), - 'fields' => array('*', 'a1.a2.a3.a4'), - 'result' => array('a1' => array('a2' => array('a3' => array('a4' => 'value1', 'a5' => 'value2')))), - ), - 'Correctly filters deep-nested arrays' => array( - 'data' => array('a1' => array( - 'a2' => array('a3' => array('a4' => 'value1', 'a5' => 'value2'), 'a32' => '1'), - )), - 'fields' => array('a1.a2.a3.a4'), - 'result' => array('a1' => array('a2' => array('a3' => array('a4' => 'value1')))), - ), - 'Takes keys from second level arrays' => array( - 'data' => array('scalar' => 'asd', 'array' => array( - array('item1' => 'asd', 'item2' => 'qwe', 'item3' => array('a', 'b')), - array('item1' => 'qwe', 'item2' => 'rty', 'item3' => array('c', 'd')), - array('item1' => 'fgh', 'item2' => 'yui', 'item3' => array('e', 'f')), - )), - 'fields' => array('array.item1', 'array.item3'), - 'result' => array('array' => array( - array('item1' => 'asd', 'item3' => array('a', 'b')), - array('item1' => 'qwe', 'item3' => array('c', 'd')), - array('item1' => 'fgh', 'item3' => array('e', 'f')), - )), - ), - 'Takes several fields from one item' => array( - 'data' => array('a1' => '1', 'a2' => '2', 'a3' => '3', 'a4' => '4'), - 'fields' => array('a1,a4', 'a2'), - 'result' => array('a1' => '1', 'a2' => '2', 'a4' => '4'), - ), - 'Leaves curly braces if all items are filtered' => array( - 'data' => array('a1' => '1', 'a2' => '2', 'a3' => '3', 'a4' => '4'), - 'fields' => array('b1'), - 'result' => new \ArrayObject(), - ), - 'Leaves simple array if all items are filtered' => array( - 'data' => array('a1' => array('a', 'b', 'c')), - 'fields' => array('a1.b1'), - 'result' => array('a1' => array('a', 'b', 'c')), - ), + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1', 'key2' => 'value2'], + ], + ], + 'Do not take with key 0' => [ + 'data' => ['payments' => [ + '0' => ['id' => 123, 'description' => 'abc1'], + 'asd' => ['id' => 124, 'description' => 'abc2'], + 'b' => ['id' => 125, 'description' => 'abc3'], + ]], + 'fields' => ['payments.asd'], + 'result' => ['payments' => [ + 'asd' => ['id' => 124, 'description' => 'abc2'], + ]], + ], + 'Takes numeric keys' => [ + 'data' => ['payments' => [ + '1' => ['id' => 123, 'description' => 'abc1'], + 'asd' => ['id' => 124, 'description' => 'abc2'], + 'b' => ['id' => 125, 'description' => 'abc3'], + ]], + 'fields' => ['payments.1'], + 'result' => ['payments' => [ + '1' => ['id' => 123, 'description' => 'abc1'], + ]], + ], + 'Filters for array items' => [ + 'data' => ['payments' => [ + ['id' => 123, 'description' => 'abc1'], + ['id' => 124, 'description' => 'abc2'], + ['id' => 125, 'description' => 'abc3'], + ]], + 'fields' => ['payments.id'], + 'result' => ['payments' => [ + ['id' => 123], + ['id' => 124], + ['id' => 125], + ]], + ], + 'Filters for array items at top level' => [ + 'data' => [ + ['id' => 123, 'description' => 'abc1'], + ['id' => 124, 'description' => 'abc2'], + ['id' => 125, 'description' => 'abc3'], + ], + 'fields' => ['id'], + 'result' => [ + ['id' => 123], + ['id' => 124], + ['id' => 125], + ], + ], + 'Correctly gets associative arrays' => [ + 'data' => ['payments' => [ + ['id' => 123, 'description' => 'abc1'], + ['id' => 124, 'description' => 'abc2'], + 5 => ['id' => 125, 'description' => 'abc3'], + ]], + 'fields' => ['payments.id'], + 'result' => ['payments' => new ArrayObject()], + ], + 'Takes all fields if wildcard on parent specified' => [ + 'data' => ['a1' => ['a2' => ['a3' => ['a4' => 'value1', 'a5' => 'value2']]]], + 'fields' => ['*', 'a1.a2.a3.a4'], + 'result' => ['a1' => ['a2' => ['a3' => ['a4' => 'value1', 'a5' => 'value2']]]], + ], + 'Correctly filters deep-nested arrays' => [ + 'data' => ['a1' => [ + 'a2' => ['a3' => ['a4' => 'value1', 'a5' => 'value2'], 'a32' => '1'], + ]], + 'fields' => ['a1.a2.a3.a4'], + 'result' => ['a1' => ['a2' => ['a3' => ['a4' => 'value1']]]], + ], + 'Takes keys from second level arrays' => [ + 'data' => ['scalar' => 'asd', 'array' => [ + ['item1' => 'asd', 'item2' => 'qwe', 'item3' => ['a', 'b']], + ['item1' => 'qwe', 'item2' => 'rty', 'item3' => ['c', 'd']], + ['item1' => 'fgh', 'item2' => 'yui', 'item3' => ['e', 'f']], + ]], + 'fields' => ['array.item1', 'array.item3'], + 'result' => ['array' => [ + ['item1' => 'asd', 'item3' => ['a', 'b']], + ['item1' => 'qwe', 'item3' => ['c', 'd']], + ['item1' => 'fgh', 'item3' => ['e', 'f']], + ]], + ], + 'Takes several fields from one item' => [ + 'data' => ['a1' => '1', 'a2' => '2', 'a3' => '3', 'a4' => '4'], + 'fields' => ['a1,a4', 'a2'], + 'result' => ['a1' => '1', 'a2' => '2', 'a4' => '4'], + ], + 'Leaves curly braces if all items are filtered' => [ + 'data' => ['a1' => '1', 'a2' => '2', 'a3' => '3', 'a4' => '4'], + 'fields' => ['b1'], + 'result' => new ArrayObject(), + ], + 'Leaves simple array if all items are filtered' => [ + 'data' => ['a1' => ['a', 'b', 'c']], + 'fields' => ['a1.b1'], + 'result' => ['a1' => ['a', 'b', 'c']], + ], // todo: -// 'Takes curly braces' => array( -// 'data' => array('a1' => '1', 'a2' => '2', 'a3' => array( +// 'Takes curly braces' => [ +// 'data' => ['a1' => '1', 'a2' => '2', 'a3' => [ // 'a31' => '31', // 'a32' => '32', -// 'a33' => array( -// array('a331' => '331a', 'a332' => '332a', 'a333' => '333a', 'a334' => '334a'), -// array('a331' => '331b', 'a332' => '332b', 'a333' => '333b', 'a334' => '334a'), -// array('a331' => '331c', 'a332' => '332c', 'a333' => '333c', 'a334' => '334a'), -// ), -// ), 'a4' => '4'), -// 'fields' => array('a3.{a31,a33.a331,a33.{a333}}', '{a1,a4},a3.a33.a334'), -// 'result' => array('a1' => '1', 'a3' => array( +// 'a33' => [ +// ['a331' => '331a', 'a332' => '332a', 'a333' => '333a', 'a334' => '334a'], +// ['a331' => '331b', 'a332' => '332b', 'a333' => '333b', 'a334' => '334a'], +// ['a331' => '331c', 'a332' => '332c', 'a333' => '333c', 'a334' => '334a'], +// ], +// ], 'a4' => '4'], +// 'fields' => ['a3.{a31,a33.a331,a33.{a333}}', '{a1,a4},a3.a33.a334'], +// 'result' => ['a1' => '1', 'a3' => [ // 'a31' => '31', -// 'a33' => array( -// array('a331' => '331a', 'a333' => '333a', 'a334' => '334a'), -// array('a331' => '331b', 'a333' => '333b', 'a334' => '334a'), -// array('a331' => '331c', 'a333' => '333c', 'a334' => '334a'), -// ), -// ), 'a4' => '4'), -// ), - ); +// 'a33' => [ +// ['a331' => '331a', 'a333' => '333a', 'a334' => '334a'], +// ['a331' => '331b', 'a333' => '333b', 'a334' => '334a'], +// ['a331' => '331c', 'a333' => '333c', 'a334' => '334a'], +// ], +// ], 'a4' => '4'], +// ], + ]; } public function filterWithScopeProvider() { - $simple = array( + $simple = [ 'key1' => 'value1', 'key2' => 'value2', - ); - $complex = array( + ]; + $complex = [ 'key1' => 'value1', 'key2' => 'value2', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1', 'key2' => 'value2'), - ); + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1', 'key2' => 'value2'], + ]; - return array( - 'Matches everything if * provided' => array( + return [ + 'Matches everything if * provided' => [ 'data' => $simple, - 'fields' => array('*'), - 'scope' => array('scope'), + 'fields' => ['*'], + 'scope' => ['scope'], 'result' => $simple, - ), - 'Filters second level data' => array( + ], + 'Filters second level data' => [ 'data' => $complex, - 'fields' => array('key0.key1', 'key0.key3', 'key0.key4.key1'), - 'scope' => array('key0'), - 'result' => array( + 'fields' => ['key0.key1', 'key0.key3', 'key0.key4.key1'], + 'scope' => ['key0'], + 'result' => [ 'key1' => 'value1', - 'key3' => array('value1', 'value2'), - 'key4' => array('key1' => 'value1'), - ), - ), - 'Correctly gets associative arrays' => array( - 'data' => array('payments' => array( - array('id' => 123, 'description' => 'abc1'), - array('id' => 124, 'description' => 'abc2'), - 5 => array('id' => 125, 'description' => 'abc3'), - )), - 'fields' => array('scope.payments.id'), - 'scope' => array('scope'), - 'result' => array('payments' => new \ArrayObject()), - ), - 'Takes all fields if wildcard on parent specified' => array( - 'data' => array('a1' => array('a2' => array('a3' => array('a4' => 'value1', 'a5' => 'value2')))), - 'fields' => array('*', 'scope.a1.a2.a3.a4'), - 'scope' => array('scope'), - 'result' => array('a1' => array('a2' => array('a3' => array('a4' => 'value1', 'a5' => 'value2')))), - ), - 'Ignores other fields' => array( - 'data' => array('a1' => array('a2' => array('a3' => array('a4' => 'value1', 'a5' => 'value2')))), - 'fields' => array('a0.a1.a2.a3.a4', 'aa.a1.a2.a3.a5'), - 'scope' => array('a0'), - 'result' => array('a1' => array('a2' => array('a3' => array('a4' => 'value1')))), - ), - 'Takes nested scope' => array( - 'data' => array(array('a4' => 'value1', 'a5' => 'value2')), - 'fields' => array('a0.a1.a2.a3.a4', 'aa.a1.a2.a3.a5'), - 'scope' => array('a0', 'a1', 'a2', 'a3'), - 'result' => array(array('a4' => 'value1')), - ), - 'Filters if on another branch' => array( - 'data' => array(array('a4' => 'value1', 'a5' => 'value2')), - 'fields' => array('a1.a2'), - 'scope' => array('a2'), - 'result' => array(array()), - ), - ); + 'key3' => ['value1', 'value2'], + 'key4' => ['key1' => 'value1'], + ], + ], + 'Correctly gets associative arrays' => [ + 'data' => ['payments' => [ + ['id' => 123, 'description' => 'abc1'], + ['id' => 124, 'description' => 'abc2'], + 5 => ['id' => 125, 'description' => 'abc3'], + ]], + 'fields' => ['scope.payments.id'], + 'scope' => ['scope'], + 'result' => ['payments' => new ArrayObject()], + ], + 'Takes all fields if wildcard on parent specified' => [ + 'data' => ['a1' => ['a2' => ['a3' => ['a4' => 'value1', 'a5' => 'value2']]]], + 'fields' => ['*', 'scope.a1.a2.a3.a4'], + 'scope' => ['scope'], + 'result' => ['a1' => ['a2' => ['a3' => ['a4' => 'value1', 'a5' => 'value2']]]], + ], + 'Ignores other fields' => [ + 'data' => ['a1' => ['a2' => ['a3' => ['a4' => 'value1', 'a5' => 'value2']]]], + 'fields' => ['a0.a1.a2.a3.a4', 'aa.a1.a2.a3.a5'], + 'scope' => ['a0'], + 'result' => ['a1' => ['a2' => ['a3' => ['a4' => 'value1']]]], + ], + 'Takes nested scope' => [ + 'data' => [['a4' => 'value1', 'a5' => 'value2']], + 'fields' => ['a0.a1.a2.a3.a4', 'aa.a1.a2.a3.a5'], + 'scope' => ['a0', 'a1', 'a2', 'a3'], + 'result' => [['a4' => 'value1']], + ], + 'Filters if on another branch' => [ + 'data' => [['a4' => 'value1', 'a5' => 'value2']], + 'fields' => ['a1.a2'], + 'scope' => ['a2'], + 'result' => [[]], + ], + ]; } } diff --git a/tests/Fixtures/OwnConstructorFilter.php b/tests/Fixtures/OwnConstructorFilter.php new file mode 100644 index 0000000..e6d02ce --- /dev/null +++ b/tests/Fixtures/OwnConstructorFilter.php @@ -0,0 +1,23 @@ +status = $status; + } +} diff --git a/tests/Normalizer/DateNormalizerTest.php b/tests/Normalizer/DateNormalizerTest.php index 51b7abc..695739f 100644 --- a/tests/Normalizer/DateNormalizerTest.php +++ b/tests/Normalizer/DateNormalizerTest.php @@ -2,6 +2,8 @@ namespace Paysera\Component\Serializer\Tests\Normalizer; +use DateTime; +use DateTimeZone; use Paysera\Component\Serializer\Exception\InvalidDataException; use Paysera\Component\Serializer\Normalizer\DateNormalizer; use PHPUnit\Framework\TestCase; @@ -19,28 +21,28 @@ public function testMapToEntity_when_no_correction_by_timezone_needed_then_date_ $service = new DateNormalizer('Y-m-d H:i:s'); $result = $service->mapToEntity('2013-02-01 12:00:00'); - $this->assertEquals(new \DateTime('2013-02-01 12:00:00', new \DateTimeZone('Etc/GMT-2')), $result); + $this->assertEquals(new DateTime('2013-02-01 12:00:00', new DateTimeZone('Etc/GMT-2')), $result); - $result = $service->mapFromEntity(new \DateTime('2013-02-01 12:00:00')); + $result = $service->mapFromEntity(new DateTime('2013-02-01 12:00:00')); $this->assertEquals('2013-02-01 12:00:00', $result); } public function testMapToEntity_when_correction_by_timezone_needed_then_date_modified() { - $service = new DateNormalizer('Y-m-d H:i:s', new \DateTimeZone('Etc/GMT+0')); + $service = new DateNormalizer('Y-m-d H:i:s', new DateTimeZone('Etc/GMT+0')); $result = $service->mapToEntity('2013-02-01 12:00:00'); - $this->assertEquals(new \DateTime('2013-02-01 14:00:00'), $result); + $this->assertEquals(new DateTime('2013-02-01 14:00:00'), $result); - $result = $service->mapFromEntity(new \DateTime('2013-02-01 14:00:00')); + $result = $service->mapFromEntity(new DateTime('2013-02-01 14:00:00')); $this->assertEquals('2013-02-01 12:00:00', $result); } public function testMapToEntity_original_entity_not_modified_when_mapping_from_entity() { - $service = new DateNormalizer('Y-m-d H:i:s', new \DateTimeZone('Etc/GMT+0')); + $service = new DateNormalizer('Y-m-d H:i:s', new DateTimeZone('Etc/GMT+0')); - $datetimeOriginal = new \DateTime('2013-02-01 14:00:00'); + $datetimeOriginal = new DateTime('2013-02-01 14:00:00'); $datetime = clone $datetimeOriginal; $service->mapFromEntity($datetime); @@ -49,7 +51,7 @@ public function testMapToEntity_original_entity_not_modified_when_mapping_from_e public function testMapToEntity_mapping_from_null_entity_returns_null() { - $service = new DateNormalizer('Y-m-d H:i:s', new \DateTimeZone('Etc/GMT+0')); + $service = new DateNormalizer('Y-m-d H:i:s', new DateTimeZone('Etc/GMT+0')); $datetime = null; $result = $service->mapFromEntity($datetime); @@ -59,10 +61,35 @@ public function testMapToEntity_mapping_from_null_entity_returns_null() public function testMapToEntity_invalid_date_throws_exception() { - $service = new DateNormalizer('Y-m-d H:i:s', new \DateTimeZone('Etc/GMT+0')); + $service = new DateNormalizer('Y-m-d H:i:s', new DateTimeZone('Etc/GMT+0')); $datetime = null; $this->expectException(InvalidDataException::class); $service->mapToEntity('2013-02-31 12:00:00'); } + + public function testMapToEntity_null_date_throws_exception_without_deprecation() + { + $service = new DateNormalizer('Y-m-d H:i:s', new DateTimeZone('Etc/GMT+0')); + + $deprecations = []; + set_error_handler( + function ($errno, $errstr) use (&$deprecations) { + $deprecations[] = $errstr; + return true; + }, + E_DEPRECATED + ); + + try { + $service->mapToEntity(null); + $this->fail('Expected InvalidDataException to be thrown'); + } catch (InvalidDataException $exception) { + $this->assertSame('Date must be provided', $exception->getMessage()); + } finally { + restore_error_handler(); + } + + $this->assertSame([], $deprecations); + } } diff --git a/tests/Normalizer/DistributedNormalizerTest.php b/tests/Normalizer/DistributedNormalizerTest.php new file mode 100644 index 0000000..700a506 --- /dev/null +++ b/tests/Normalizer/DistributedNormalizerTest.php @@ -0,0 +1,42 @@ +fieldNormalizers[$key]), which is simply false + * for every key. + * + * @dataProvider fieldMapProvider + */ + public function testFieldMapsDefaultToArrayWhenConstructorIsBypassed($property) + { + $normalizer = (new ReflectionClass(DistributedNormalizer::class))->newInstanceWithoutConstructor(); + + $reflectionProperty = new ReflectionProperty(DistributedNormalizer::class, $property); + $reflectionProperty->setAccessible(true); + + $this->assertSame([], $reflectionProperty->getValue($normalizer)); + } + + public function fieldMapProvider() + { + return [ + 'fieldAccessors' => ['fieldAccessors'], + 'fieldDefault' => ['fieldDefault'], + 'fieldNormalizers' => ['fieldNormalizers'], + ]; + } +}