Skip to content

Commit

Permalink
fix: handle assert for php8 Union and Intersection
Browse files Browse the repository at this point in the history
  • Loading branch information
Cristoforo Cervino committed Aug 28, 2023
1 parent 6e544a8 commit 8455670
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 20 deletions.
80 changes: 63 additions & 17 deletions src/Assert.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,7 @@ public static function assertTargetCallableHasRightTypeHintedArguments(
throw new TargetCallableArgumentException(\sprintf('Callable for option "%s" must have first argument nullable', TargetCallbackExtension::NAME));
}

$targetParamClass = $targetParam->getType() && !$targetParam->getType()->isBuiltin() ? new \ReflectionClass($targetParam->getType()->getName()) : null;
if (
null !== $target
&& $targetParam->hasType()
&& (null !== $targetParamClass ?
!\is_a($target, $targetParamClass->getName(), true) :
// @phpstan-ignore-next-line
($targetParamType = $targetParam->getType()) !== null && \get_debug_type($target) !== $targetParamType->getName())) {
if (!self::callableAcceptsValue($r, 0, $target)) {
throw new TargetCallableArgumentException(\sprintf('Callable for option "%s" must have first argument type-hinted as "%s"', TargetCallbackExtension::NAME, \get_debug_type($target)));
}

Expand All @@ -68,20 +61,19 @@ public static function assertTargetCallableHasRightTypeHintedArguments(
throw new TargetCallableArgumentException(\sprintf('Callable for option "%s" must have second argument nullable', TargetCallbackExtension::NAME));
}

$formDataParamClass = $formDataParam->getType() && !$formDataParam->getType()->isBuiltin() ? new \ReflectionClass($formDataParam->getType()->getName()) : null;
if (
null !== $formData
&& $formDataParam->hasType()
&& (null !== $formDataParamClass ?
!\is_a($formData, $formDataParamClass->getName(), true) :
// @phpstan-ignore-next-line
($formDataParamType = $formDataParam->getType()) !== null && \get_debug_type($formData) !== $formDataParamType->getName())) {
if (!self::callableAcceptsValue($r, 1, $formData)) {
throw new TargetCallableArgumentException(\sprintf('Callable for option "%s" must have second argument type-hinted as "%s"', TargetCallbackExtension::NAME, \get_debug_type($formData)));
}

// Checking $form argument
if (null !== $formParam) {
$formParamClass = $formParam->getType() && !$formParam->getType()->isBuiltin() ? new \ReflectionClass($formParam->getType()->getName()) : null;
$formParamClass = null;
$formParamType = $formParam->getType();
if ($formParamType instanceof \ReflectionNamedType) {
/** @var class-string<object> $formParamTypeName */
$formParamTypeName = $formParamType->getName();
$formParamClass = !$formParamType->isBuiltin() ? new \ReflectionClass($formParamTypeName) : null;
}
if (
$formParam->hasType()
&& (
Expand All @@ -93,4 +85,58 @@ public static function assertTargetCallableHasRightTypeHintedArguments(
}
}
}

/**
* @param mixed $paramValue
*/
protected static function callableAcceptsValue(\ReflectionFunction $r, int $paramPosition, $paramValue): bool
{
foreach ($r->getParameters() as $parameter) {
if ($parameter->getPosition() !== $paramPosition) {
continue;
}

$type = $parameter->getType();
if ($type instanceof \ReflectionNamedType) {
return self::isCompatibleWithType($type->getName(), $paramValue) || ($type->allowsNull() && \is_null($paramValue));
}

if (\version_compare(PHP_VERSION, '8.0.0') >= 0 && $type instanceof \ReflectionUnionType) {
foreach ($type->getTypes() as $t) {
if (self::isCompatibleWithType($t->getName(), $paramValue) || ($t->allowsNull() && \is_null($paramValue))) {
return true;
}
}

return false;
}

if (\version_compare(PHP_VERSION, '8.1.0') >= 0 && $type instanceof \ReflectionIntersectionType) {
foreach ($type->getTypes() as $t) {
if (!self::isCompatibleWithType($t->getName(), $paramValue)) { /* intersection types do not support nullables for now */
return false;
}
}

return true;
}

return false;
}

return false;
}

/**
* @param mixed|object $value
*/
protected static function isCompatibleWithType(string $type, $value): bool
{
if (\is_object($value)) {
// @phpstan-ignore-next-line
return \get_class($value) === $type || \is_subclass_of($value, $type);
}

return \gettype($value) === $type;
}
}
2 changes: 1 addition & 1 deletion src/PageFilterManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function __construct(FormFactoryInterface $formFactory)
public function createAndHandleFilter(
string $formType,
&$target,
?Request $request = null,
Request $request = null,
$data = null,
array $options = [],
string $formName = ''
Expand Down
2 changes: 1 addition & 1 deletion src/PageFilterManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface PageFilterManagerInterface
public function createAndHandleFilter(
string $formType,
&$target,
?Request $request = null,
Request $request = null,
$data = null,
array $options = [],
string $formName = ''
Expand Down
30 changes: 30 additions & 0 deletions tests/Form/DumbObjectIntersectionTypeHint1PageFilterType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Andante\PageFilterFormBundle\Tests\Form;

use Andante\PageFilterFormBundle\Form\PageFilterType;
use Andante\PageFilterFormBundle\Tests\Model\Test1Interface;
use Andante\PageFilterFormBundle\Tests\Model\Test2Interface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class DumbObjectIntersectionTypeHint1PageFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (\version_compare(PHP_VERSION, '8.1.0') >= 0) {
$builder->add('child1', TextType::class, [
'target_callback' => function (Test1Interface&Test2Interface $obj, ?int $a): void {
},
]);
}
}

public function getParent()
{
return PageFilterType::class;
}
}
30 changes: 30 additions & 0 deletions tests/Form/DumbObjectUnionTypeHint1PageFilterType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Andante\PageFilterFormBundle\Tests\Form;

use Andante\PageFilterFormBundle\Form\PageFilterType;
use Andante\PageFilterFormBundle\Tests\Model\Test1Interface;
use Andante\PageFilterFormBundle\Tests\Model\Test2Interface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class DumbObjectUnionTypeHint1PageFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (\version_compare(PHP_VERSION, '8.0.0') >= 0) {
$builder->add('child1', TextType::class, [
'target_callback' => function (Test1Interface|Test2Interface $obj, ?int $a): void {
},
]);
}
}

public function getParent()
{
return PageFilterType::class;
}
}
42 changes: 42 additions & 0 deletions tests/Functional/PageFilterManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
use Andante\PageFilterFormBundle\PageFilterManagerInterface;
use Andante\PageFilterFormBundle\Tests\Form\DumbArrayByValuePageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbArrayPageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbObjectIntersectionTypeHint1PageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbObjectNotEnoughParametersPageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbObjectPageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbObjectUnionTypeHint1PageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbObjectWrongTypeHint1PageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbObjectWrongTypeHint2PageFilterType;
use Andante\PageFilterFormBundle\Tests\Form\DumbObjectWrongTypeHint3PageFilterType;
Expand Down Expand Up @@ -135,6 +137,46 @@ public function testCreateAndHandleFilterWithObjectWrongTypeHint2(): void
);
}

public function testCreateAndHandleFilterWithObjectUnionTypeHint1(): void
{
if (\version_compare(PHP_VERSION, '8.0.0') >= 0) {
/** @var PageFilterManagerInterface $filterManager */
$filterManager = self::getContainer()->get(PageFilterManager::class);
$fakeQueryBuilder = new \stdClass();
$this->expectException(TargetCallableArgumentException::class);
$this->expectExceptionMessage('argument type-hinted');
$filterManager->createAndHandleFilter(
DumbObjectUnionTypeHint1PageFilterType::class,
$fakeQueryBuilder,
Request::create('/', 'GET', [
'child1' => 'newCriteriaSearch1',
])
);
} else {
self::assertEquals(true, true);
}
}

public function testCreateAndHandleFilterWithObjectIntersectionTypeHint1(): void
{
if (\version_compare(PHP_VERSION, '8.1.0') >= 0) {
/** @var PageFilterManagerInterface $filterManager */
$filterManager = self::getContainer()->get(PageFilterManager::class);
$fakeQueryBuilder = new \stdClass();
$this->expectException(TargetCallableArgumentException::class);
$this->expectExceptionMessage('argument type-hinted');
$filterManager->createAndHandleFilter(
DumbObjectIntersectionTypeHint1PageFilterType::class,
$fakeQueryBuilder,
Request::create('/', 'GET', [
'child1' => 'newCriteriaSearch1',
])
);
} else {
self::assertEquals(true, true);
}
}

public function testCreateAndHandleFilterWithObjectWrongTypeHint3(): void
{
/** @var PageFilterManagerInterface $filterManager */
Expand Down
2 changes: 1 addition & 1 deletion tests/HttpKernel/AndantePageFilterFormKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void

public function getCacheDir(): string
{
return \sprintf(__DIR__.'/../../var/cache/test/%s/', \hash('crc32b', ((string) \json_encode($this->configs))));
return \sprintf(__DIR__.'/../../var/cache/test/%s/', \hash('crc32b', (string) \json_encode($this->configs)));
}

public function getLogDir(): string
Expand Down
9 changes: 9 additions & 0 deletions tests/Model/ExampleModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Andante\PageFilterFormBundle\Tests\Model;

class ExampleModel extends \stdClass implements Test1Interface, Test2Interface
{
}
9 changes: 9 additions & 0 deletions tests/Model/Test1Interface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Andante\PageFilterFormBundle\Tests\Model;

interface Test1Interface
{
}
9 changes: 9 additions & 0 deletions tests/Model/Test2Interface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Andante\PageFilterFormBundle\Tests\Model;

interface Test2Interface
{
}

0 comments on commit 8455670

Please sign in to comment.