diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 81458a2..cdeaece 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -66,12 +66,25 @@ jobs:
mkdir -p build/logs
php vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover=build/logs/clover.xml
+ # Infection 0.32.x requires PHP 8.3+, so keep the linked PHPStan/Infection checks on the basic 8.3 job.
- name: Run phpstan
- continue-on-error: true
- if: ${{ matrix.php == '8.0' }}
+ if: ${{ matrix.php == '8.3' && matrix.composer == 'basic' }}
run: |
php vendor/bin/phpstan analyse
+ - name: Install Infection
+ if: ${{ matrix.php == '8.3' && matrix.composer == 'basic' }}
+ run: |
+ curl --fail --silent --show-error --location \
+ --output infection.phar \
+ https://github.com/infection/infection/releases/download/0.32.7/infection.phar
+ chmod +x infection.phar
+
+ - name: Run Infection
+ if: ${{ matrix.php == '8.3' && matrix.composer == 'basic' }}
+ run: |
+ php infection.phar --threads=max --min-msi=0 --min-covered-msi=0 --no-progress
+
- name: Upload coverage results to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/infection.json.dist b/infection.json.dist
index 7b38b1f..765f751 100644
--- a/infection.json.dist
+++ b/infection.json.dist
@@ -8,8 +8,9 @@
"phpUnit": {
"customPath": "vendor\/bin\/phpunit"
},
+ "staticAnalysisTool": "phpstan",
"tmpDir": "build/infection/",
"logs": {
"text": "infection-log.txt"
}
-}
\ No newline at end of file
+}
diff --git a/phpstan-fixtures.neon b/phpstan-fixtures.neon
new file mode 100644
index 0000000..ff963b3
--- /dev/null
+++ b/phpstan-fixtures.neon
@@ -0,0 +1,12 @@
+parameters:
+ level: 8
+ reportUnmatchedIgnoredErrors: true
+ paths:
+ - %currentWorkingDirectory%/src/
+ - %currentWorkingDirectory%/tests/
+
+services:
+ -
+ class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension
+ tags:
+ - phpstan.broker.dynamicStaticMethodReturnTypeExtension
diff --git a/phpstan.neon b/phpstan.neon
index ff963b3..f5b41c4 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,8 +2,12 @@ parameters:
level: 8
reportUnmatchedIgnoredErrors: true
paths:
+ # Keep the repo-wide CI pass focused on production code; fixture-style PHPStan tests run via phpstan-fixtures.neon.
- %currentWorkingDirectory%/src/
- - %currentWorkingDirectory%/tests/
+ ignoreErrors:
+ -
+ message: '#^Parameter \#1 \$data of static method Arrayy\\Arrayy<.*>::create\(\) expects .*, .* given\.$#'
+ path: %currentWorkingDirectory%/src/Arrayy.php
services:
-
diff --git a/src/Arrayy.php b/src/Arrayy.php
index 55f51ad..f9f63f4 100644
--- a/src/Arrayy.php
+++ b/src/Arrayy.php
@@ -282,6 +282,7 @@ public function add($value, $key = null)
);
}
+ /* @phpstan-ignore argument.type */
$this->internalSet($key, $value);
return $this;
@@ -788,9 +789,11 @@ public function &offsetGet($offset)
$value = null;
if ($this->offsetExists($offset)) {
+ /* @phpstan-ignore argument.type, argument.templateType */
$value = &$this->__get($offset);
}
+ /* @phpstan-ignore return.type */
return $value;
}
@@ -1052,7 +1055,7 @@ public function unserialize($string): self
* @return $this
*
(Mutable) Return this Arrayy object, with the appended values.
*
- * @phpstan-param array $values
+ * @phpstan-param array $values
* @phpstan-param TKey|null $key
* @phpstan-return static
*/
@@ -1067,6 +1070,7 @@ public function appendArrayValues(array $values, $key = null)
\is_array($this->array[$key])
) {
foreach ($values as $value) {
+ /* @phpstan-ignore assign.propertyType */
$this->array[$key][] = $value;
}
} else {
@@ -7451,7 +7455,7 @@ protected function array_keys_recursive(
*
* @return void
*
- * @phpstan-param array|null $currentOffset
+ * @phpstan-param array|null $currentOffset
* @psalm-mutation-free
*/
protected function callAtPath($path, $callable, &$currentOffset = null)
@@ -7487,8 +7491,8 @@ protected function callAtPath($path, $callable, &$currentOffset = null)
/**
* Extracts the value of the given property or method from the object.
*
- * @param static $object
- * The object to extract the value from.
+ * @param mixed $object
+ * The Arrayy instance, object, or other value from which to extract the property or method value.
* @param string $keyOrPropertyOrMethod
* The property or method for which the
* value should be extracted.
@@ -7498,11 +7502,10 @@ protected function callAtPath($path, $callable, &$currentOffset = null)
* @return mixed
* The value extracted from the specified property or method.
*
- * @phpstan-param self $object
*/
- final protected function extractValue(self $object, string $keyOrPropertyOrMethod)
+ final protected function extractValue($object, string $keyOrPropertyOrMethod)
{
- if (isset($object[$keyOrPropertyOrMethod])) {
+ if ($object instanceof self && isset($object[$keyOrPropertyOrMethod])) {
$return = $object->get($keyOrPropertyOrMethod);
if ($return instanceof self) {
@@ -7512,11 +7515,11 @@ final protected function extractValue(self $object, string $keyOrPropertyOrMetho
return $return;
}
- if (\property_exists($object, $keyOrPropertyOrMethod)) {
+ if (\is_object($object) && \property_exists($object, $keyOrPropertyOrMethod)) {
return $object->{$keyOrPropertyOrMethod};
}
- if (\method_exists($object, $keyOrPropertyOrMethod)) {
+ if (\is_object($object) && \method_exists($object, $keyOrPropertyOrMethod)) {
return $object->{$keyOrPropertyOrMethod}();
}
@@ -8049,6 +8052,14 @@ protected function internalRemove($key): bool
$key = \array_shift($path);
}
+ if ($key === null) {
+ return false;
+ }
+
+ if (\is_float($key)) {
+ return false;
+ }
+
unset($this->array[$key]);
return true;
diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php
index 58fbcd1..2d7b222 100644
--- a/src/Collection/AbstractCollection.php
+++ b/src/Collection/AbstractCollection.php
@@ -317,10 +317,12 @@ public static function createFromJsonMapper(string $json)
if (\is_array($jsonObject)) {
foreach ($jsonObject as $jsonObjectSingle) {
$collectionData = $mapper->map($jsonObjectSingle, $type);
+ /** @phpstan-var T $collectionData */
$return->add($collectionData);
}
} else {
$collectionData = $mapper->map($jsonObject, $type);
+ /** @phpstan-var T $collectionData */
$return->add($collectionData);
}
} else {
diff --git a/src/Create.php b/src/Create.php
index 9241591..45d15fb 100644
--- a/src/Create.php
+++ b/src/Create.php
@@ -17,7 +17,10 @@
*/
function create($data): Arrayy
{
- return new Arrayy($data);
+ /** @var Arrayy> $array */
+ $array = new Arrayy($data);
+
+ return $array;
}
}
diff --git a/src/Mapper/Json.php b/src/Mapper/Json.php
index a5724e0..d09e769 100644
--- a/src/Mapper/Json.php
+++ b/src/Mapper/Json.php
@@ -19,7 +19,7 @@ final class Json
* Override class names that JsonMapper uses to create objects.
* Useful when your setter methods accept abstract classes or interfaces.
*
- * @var array
+ * @var array
*/
public $classMap = [];
@@ -33,7 +33,7 @@ final class Json
* 2. Name of the unknown JSON property
* 3. JSON value of the property
*
- * @var callable
+ * @var null|callable(object, string, mixed): void
*/
public $undefinedPropertyHandler;
@@ -41,14 +41,14 @@ final class Json
* Runtime cache for inspected classes. This is particularly effective if
* mapArray() is called with a large number of objects
*
- * @var array property inspection result cache
+ * @var array> property inspection result cache
*/
private $arInspectedClasses = [];
/**
* Map data all data in $json into the given $object instance.
*
- * @param object|iterable $json
+ * @param object|iterable $json
* JSON object structure from json_decode()
* @param object|string $object
* Object to map $json data into
@@ -58,7 +58,8 @@ final class Json
*
* @see mapArray()
*
- * @template TObject
+ * @template TObject of object
+ * @phpstan-param object|iterable $json
* @phpstan-param TObject|class-string $object
* Object to map $json data into.
* @phpstan-return TObject
@@ -79,6 +80,11 @@ public function map($json, $object)
$strClassName = \get_class($object);
$rc = new \ReflectionClass($object);
$strNs = $rc->getNamespaceName();
+
+ if (\is_object($json) && !($json instanceof \Traversable)) {
+ $json = \get_object_vars($json);
+ }
+
foreach ($json as $key => $jsonValue) {
$key = $this->getSafeName($key);
@@ -95,9 +101,8 @@ public function map($json, $object)
) = $this->arInspectedClasses[$strClassName][$key];
if (!$hasProperty) {
- if (\is_callable($this->undefinedPropertyHandler)) {
- \call_user_func(
- $this->undefinedPropertyHandler,
+ if ($this->undefinedPropertyHandler !== null) {
+ ($this->undefinedPropertyHandler)(
$object,
$key,
$jsonValue
@@ -111,7 +116,7 @@ public function map($json, $object)
continue;
}
- if ($this->isNullable($type)) {
+ if ($type !== null && $this->isNullable($type)) {
if ($jsonValue === null) {
$this->setProperty($object, $accessor, null);
@@ -247,7 +252,7 @@ public function map($json, $object)
/**
* Map an array
*
- * @param array $json JSON array structure from json_decode()
+ * @param array $json JSON array structure from json_decode()
* @param mixed $array Array or ArrayObject that gets filled with
* data from $json
* @param string|null $class Class name for children objects.
@@ -261,6 +266,8 @@ public function map($json, $object)
* @pslam-param null|class-string $class
*
* @return mixed Mapped $array is returned
+ *
+ * @phpstan-param array $json
*/
public function mapArray($json, $array, $class = null, $parent_key = '')
{
@@ -301,8 +308,12 @@ public function mapArray($json, $array, $class = null, $parent_key = '')
)
&&
\count($typesTmp->getTypes()) === 1
+ &&
+ \class_exists($typesTmp->getTypes()[0])
) {
- $array[$key] = $this->map($jsonValue, $typesTmp->getTypes()[0]);
+ /** @var class-string