diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 08d55aa..faeca00 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -6,18 +6,20 @@ on: jobs: phpunit: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: include: - - php: '7.4' - phpunit: '8.5' - - php: '8.0' - phpunit: '8.5' + - php: '8.1' + psalm: true - php: '8.1' phpunit: '8.5' + - php: '8.5' + psalm: true + - php: '8.5' + phpunit: '8.5' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -26,7 +28,7 @@ jobs: tools: 'composer:v2' - name: Cache Composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v5 with: path: '~/.composer/cache' key: ${{ runner.os }}-php${{ matrix.php }}-phpunit${{ matrix.phpunit }}-symfony${{ matrix.symfony }}-${{ hashFiles('**/composer.json') }} @@ -37,7 +39,13 @@ jobs: run: composer update --no-interaction --no-progress - name: Run test suite - uses: php-actions/phpunit@v3 + uses: php-actions/phpunit@v4 + if: ${{ matrix.phpunit != '' }} with: php_version: ${{ matrix.php }} version: ${{ matrix.phpunit }} + + - name: Run Psalm analysis + run: vendor/bin/psalm + if: ${{ matrix.psalm == true }} + continue-on-error: true diff --git a/.gitignore b/.gitignore index 58fb006..d338484 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.cache # Composer vendor/ composer.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6920f64..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: php -php: - - '7.1' - - '7.2' - - '7.3' - - '7.4' -install: - - composer install diff --git a/composer.json b/composer.json index a820f30..deb6741 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,11 @@ "description": "An AutoMapper for PHP", "license": "MIT", "type": "library", - "keywords": ["automapper", "converter", "object-to-object"], + "keywords": [ + "automapper", + "converter", + "object-to-object" + ], "authors": [ { "name": "Mark Gerarts", @@ -12,7 +16,7 @@ ], "autoload": { "psr-4": { - "AutoMapperPlus\\": "src/" + "AutoMapperPlus\\": "src/" } }, "autoload-dev": { @@ -22,12 +26,13 @@ } }, "require": { - "php": ">=7.4.0" + "php": ">=8.1.0" }, "require-dev": { "symfony/var-dumper": "^3.3", "symfony/debug": "^3.3", "mark-gerarts/phpstan-automapper-plus": "^0.1.0", - "phpunit/phpunit": "^7.0|^8.0" + "phpunit/phpunit": "^10.0|^11.0|12.0", + "vimeo/psalm": "^6.0" } } diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..d2bdf8e --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,26 @@ + + + + + mapToObject + + + + + map + mapMultiple + + + + + array + + + $array + + + $array + $array + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..72b4005 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/AutoMapper.php b/src/AutoMapper.php index 09172cd..93a2d9c 100644 --- a/src/AutoMapper.php +++ b/src/AutoMapper.php @@ -51,7 +51,7 @@ public static function initialize(callable $configurator): AutoMapperInterface return $mapper; } - private function push($key, $value, &$context) + private function push($key, $value, &$context): void { if (!array_key_exists($key, $context)) { $stack = []; @@ -62,15 +62,19 @@ private function push($key, $value, &$context) $context[$key] = $stack; } - private function pop($key, &$context) + private function pop($key, &$context): void { array_pop($context[$key]); } /** * @inheritdoc + * + * @psalm-suppress TooManyArguments + * Psalm borks on the missing $context on the interface, which is there + * because of backwards compatibility. */ - public function map($source, string $destinationClass, array $context = []) + public function map($source, string $targetClass, array $context = []) { if ($source === null) { return null; @@ -85,13 +89,13 @@ public function map($source, string $destinationClass, array $context = []) } } - $context[self::DESTINATION_CLASS_CONTEXT] = $destinationClass; + $context[self::DESTINATION_CLASS_CONTEXT] = $targetClass; - $mapping = $this->getMapping($sourceClass, $destinationClass); + $mapping = $this->getMapping($sourceClass, $targetClass); if ($mapping->providesCustomMapper()) { $customMapper = $this->getCustomMapper($mapping); - return $customMapper->map($source, $destinationClass, $context); + return $customMapper->map($source, $targetClass, $context); } if ($mapping->hasCustomConstructor()) { @@ -100,14 +104,14 @@ public function map($source, string $destinationClass, array $context = []) $this, $context ); - } elseif (interface_exists($destinationClass)) { + } elseif (interface_exists($targetClass)) { // If we're mapping to an interface a valid custom constructor has // to be provided. Otherwise we can't know what to do. $message = 'Mapping to an interface is not possible. Please ' . 'provide a concrete class or use mapToObject instead.'; throw new AutoMapperPlusException($message); } else { - $destinationObject = new $destinationClass; + $destinationObject = new $targetClass; } $context[self::DESTINATION_CONTEXT] = $destinationObject; @@ -128,7 +132,7 @@ public function map($source, string $destinationClass, array $context = []) */ public function mapMultiple( $sourceCollection, - string $destinationClass, + string $targetClass, array $context = [] ): array { @@ -140,7 +144,7 @@ public function mapMultiple( $mappedResults = []; foreach ($sourceCollection as $source) { - $mappedResults[] = $this->map($source, $destinationClass, $context); + $mappedResults[] = $this->map($source, $targetClass, $context); } return $mappedResults; diff --git a/src/Configuration/Options.php b/src/Configuration/Options.php index 5707201..ee40436 100644 --- a/src/Configuration/Options.php +++ b/src/Configuration/Options.php @@ -71,7 +71,8 @@ class Options private $useSubstitution = true; /** - * @var string[] + * @var bool[] + * @psalm-var array */ private $objectCrates = []; @@ -280,7 +281,7 @@ public function providesCustomMapper(): bool } /** - * @param string $className + * @param class-string $className */ public function registerObjectCrate(string $className): void { diff --git a/src/CustomMapper/CustomMapper.php b/src/CustomMapper/CustomMapper.php index e254b97..c8876d6 100644 --- a/src/CustomMapper/CustomMapper.php +++ b/src/CustomMapper/CustomMapper.php @@ -13,6 +13,10 @@ abstract class CustomMapper implements MapperInterface { /** * @inheritdoc + * + * @psalm-suppress TooManyArguments + * Psalm borks on the missing $context on the interface, which is there + * because of backwards compatibility. */ public function map($source, string $targetClass) { diff --git a/src/MapperInterface.php b/src/MapperInterface.php index 23a6df4..6c50ec8 100644 --- a/src/MapperInterface.php +++ b/src/MapperInterface.php @@ -15,10 +15,12 @@ interface MapperInterface * Maps an object to an instance of class $to, provided a mapping is * configured. * + * @template T of object * @param array|object $source * The source object. * @param string $targetClass * The target classname. + * @psalm-param class-string $targetClass * @param array $context * An arbitrary array of values that will be passed to supporting * mapping operations (e.g. MapFrom) to alter their behaviour based on @@ -26,6 +28,7 @@ interface MapperInterface * This is not explicitly required on the interface yet to preserve * backwards compatibility, but will be added in version 2.0. * @return mixed + * @psalm-return T * An instance of class $to. * @throws UnregisteredMappingException */ @@ -34,13 +37,16 @@ public function map($source, string $targetClass/**, array $context = [] */); /** * Maps properties of object $from to an existing object $to. * + * @template T of object * @param array|object $source * The source object. * @param object $destination * The target object. + * @psalm-param T $destination * @param array $context * See MapperInterface::map() * @return mixed + * @psalm-return T * $to, with properties copied from $from. * @throws UnregisteredMappingException */ diff --git a/src/MappingOperation/DefaultMappingOperation.php b/src/MappingOperation/DefaultMappingOperation.php index 65eb568..eeed295 100644 --- a/src/MappingOperation/DefaultMappingOperation.php +++ b/src/MappingOperation/DefaultMappingOperation.php @@ -4,6 +4,7 @@ use AutoMapperPlus\Configuration\Options; use AutoMapperPlus\NameResolver\NameResolverInterface; +use AutoMapperPlus\PropertyAccessor\PropertyAccessorInterface; use AutoMapperPlus\PropertyAccessor\PropertyReaderInterface; use AutoMapperPlus\PropertyAccessor\PropertyWriterInterface; diff --git a/src/MappingOperation/Implementations/MapToAnyOf.php b/src/MappingOperation/Implementations/MapToAnyOf.php index 51ab1a7..b4c4b65 100644 --- a/src/MappingOperation/Implementations/MapToAnyOf.php +++ b/src/MappingOperation/Implementations/MapToAnyOf.php @@ -70,6 +70,11 @@ function ($value) use ($context) { ); } + /** + * @psalm-suppress TooManyArguments + * Psalm borks on the missing $context on the interface, which is there + * because of backwards compatibility. + */ private function mapSingle($item, $context) { $destinationClass = $this->getDestinationClass($item); diff --git a/src/PropertyAccessor/ArrayPropertyReader.php b/src/PropertyAccessor/ArrayPropertyReader.php index 71bdfa9..6622564 100644 --- a/src/PropertyAccessor/ArrayPropertyReader.php +++ b/src/PropertyAccessor/ArrayPropertyReader.php @@ -29,6 +29,8 @@ public function getProperty($array, string $propertyName) * @inheritdoc * * @param array $array + * + * @psalm-suppress ParamNameMismatch */ public function getPropertyNames($array): array { diff --git a/src/PropertyAccessor/PropertyReaderInterface.php b/src/PropertyAccessor/PropertyReaderInterface.php index b938404..3ea494e 100644 --- a/src/PropertyAccessor/PropertyReaderInterface.php +++ b/src/PropertyAccessor/PropertyReaderInterface.php @@ -10,14 +10,14 @@ interface PropertyReaderInterface { /** - * @param $object + * @param object|array $object * @param string $propertyName * @return bool */ public function hasProperty($object, string $propertyName): bool; /** - * @param $object + * @param object|array $object * @param string $propertyName * @return mixed */ @@ -26,7 +26,7 @@ public function getProperty($object, string $propertyName); /** * Returns a list of property names available on the object. * - * @param object $object + * @param object|array $object * @return string[] */ public function getPropertyNames($object): array;