Skip to content

[TypeInfo] Add accepts method #59291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Component/TypeInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.3
---

* Add `Type::accepts()` method

7.2
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ public function testToString()
{
$this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int()));
}

public function testAccepts()
{
$type = new BackedEnumType(DummyBackedEnum::class, Type::int());

$this->assertFalse($type->accepts('string'));
$this->assertTrue($type->accepts(DummyBackedEnum::ONE));
}
}
44 changes: 44 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,48 @@ public function testIsNullable()
$this->assertTrue((new BuiltinType(TypeIdentifier::MIXED))->isNullable());
$this->assertFalse((new BuiltinType(TypeIdentifier::INT))->isNullable());
}

public function testAccepts()
{
$this->assertFalse((new BuiltinType(TypeIdentifier::ARRAY))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::ARRAY))->accepts([]));

$this->assertFalse((new BuiltinType(TypeIdentifier::BOOL))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::BOOL))->accepts(true));

$this->assertFalse((new BuiltinType(TypeIdentifier::CALLABLE))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::CALLABLE))->accepts('strtoupper'));

$this->assertFalse((new BuiltinType(TypeIdentifier::FALSE))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::FALSE))->accepts(false));

$this->assertFalse((new BuiltinType(TypeIdentifier::FLOAT))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::FLOAT))->accepts(1.23));

$this->assertFalse((new BuiltinType(TypeIdentifier::INT))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::INT))->accepts(123));

$this->assertFalse((new BuiltinType(TypeIdentifier::ITERABLE))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::ITERABLE))->accepts([]));

$this->assertTrue((new BuiltinType(TypeIdentifier::MIXED))->accepts('string'));

$this->assertFalse((new BuiltinType(TypeIdentifier::NULL))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::NULL))->accepts(null));

$this->assertFalse((new BuiltinType(TypeIdentifier::OBJECT))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::OBJECT))->accepts(new \stdClass()));

$this->assertFalse((new BuiltinType(TypeIdentifier::RESOURCE))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::RESOURCE))->accepts(fopen('php://temp', 'r')));

$this->assertFalse((new BuiltinType(TypeIdentifier::STRING))->accepts(123));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted we are strict and don't apply type casts.

$this->assertTrue((new BuiltinType(TypeIdentifier::STRING))->accepts('string'));

$this->assertFalse((new BuiltinType(TypeIdentifier::TRUE))->accepts('string'));
$this->assertTrue((new BuiltinType(TypeIdentifier::TRUE))->accepts(true));

$this->assertFalse((new BuiltinType(TypeIdentifier::NEVER))->accepts('string'));
$this->assertFalse((new BuiltinType(TypeIdentifier::VOID))->accepts('string'));
}
}
37 changes: 37 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,41 @@ public function testToString()
$type = new CollectionType(new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()));
$this->assertEquals('array<string,bool>', (string) $type);
}

public function testAccepts()
{
$type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()));

$this->assertFalse($type->accepts(new \ArrayObject(['foo' => true, 'bar' => true])));

$this->assertTrue($type->accepts(['foo' => true, 'bar' => true]));
$this->assertFalse($type->accepts(['foo' => true, 'bar' => 123]));
$this->assertFalse($type->accepts([1 => true]));

$type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::int(), Type::bool()));

$this->assertTrue($type->accepts([1 => true]));
$this->assertFalse($type->accepts(['foo' => true]));

$type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::int(), Type::bool()), isList: true);

$this->assertTrue($type->accepts([0 => true, 1 => false]));
$this->assertFalse($type->accepts([0 => true, 2 => false]));

$type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ITERABLE), Type::string(), Type::bool()));

$this->assertTrue($type->accepts(new \ArrayObject(['foo' => true, 'bar' => true])));
$this->assertFalse($type->accepts(new \ArrayObject(['foo' => true, 'bar' => 123])));
$this->assertFalse($type->accepts(new \ArrayObject([1 => true])));

$type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ITERABLE), Type::int(), Type::bool()));

$this->assertTrue($type->accepts(new \ArrayObject([1 => true])));
$this->assertFalse($type->accepts(new \ArrayObject(['foo' => true])));

$type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ITERABLE), Type::int(), Type::bool()));

$this->assertTrue($type->accepts(new \ArrayObject([0 => true, 1 => false])));
$this->assertFalse($type->accepts(new \ArrayObject([0 => true, 1 => 'string'])));
}
}
8 changes: 8 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ public function testToString()
{
$this->assertSame(DummyEnum::class, (string) new EnumType(DummyEnum::class));
}

public function testAccepts()
{
$type = new EnumType(DummyEnum::class);

$this->assertFalse($type->accepts('string'));
$this->assertTrue($type->accepts(DummyEnum::ONE));
}
}
8 changes: 8 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,12 @@ public function testWrappedTypeIsSatisfiedBy()
$type = new GenericType(Type::builtin(TypeIdentifier::ITERABLE), Type::bool());
$this->assertFalse($type->wrappedTypeIsSatisfiedBy(static fn (Type $t): bool => 'array' === (string) $t));
}

public function testAccepts()
{
$type = new GenericType(Type::object(self::class), Type::string());

$this->assertFalse($type->accepts('string'));
$this->assertTrue($type->accepts($this));
}
}
18 changes: 18 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,22 @@ public function testToString()
$type = new IntersectionType(Type::object(\DateTime::class), Type::object(\Iterator::class), Type::object(\Stringable::class));
$this->assertSame(\sprintf('%s&%s&%s', \DateTime::class, \Iterator::class, \Stringable::class), (string) $type);
}

public function testAccepts()
{
$type = new IntersectionType(Type::object(\Traversable::class), Type::object(\Countable::class));

$traversableAndCountable = new \ArrayObject();

$countable = new class implements \Countable {
public function count(): int
{
return 1;
}
};

$this->assertFalse($type->accepts('string'));
$this->assertFalse($type->accepts($countable));
$this->assertTrue($type->accepts($traversableAndCountable));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,13 @@ public function testWrappedTypeIsSatisfiedBy()
$type = new NullableType(Type::string());
$this->assertFalse($type->wrappedTypeIsSatisfiedBy(static fn (Type $t): bool => 'int' === (string) $t));
}

public function testAccepts()
{
$type = new NullableType(Type::int());

$this->assertFalse($type->accepts('string'));
$this->assertTrue($type->accepts(123));
$this->assertTrue($type->accepts(null));
}
}
8 changes: 8 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ public function testIsIdentifiedBy()

$this->assertTrue((new ObjectType(self::class))->isIdentifiedBy('array', 'object'));
}

public function testAccepts()
{
$this->assertFalse((new ObjectType(self::class))->accepts('string'));
$this->assertFalse((new ObjectType(self::class))->accepts(new \stdClass()));
$this->assertTrue((new ObjectType(parent::class))->accepts($this));
$this->assertTrue((new ObjectType(self::class))->accepts($this));
}
}
26 changes: 26 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/TemplateTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\TypeInfo\Tests\Type;

use PHPUnit\Framework\TestCase;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\Type\TemplateType;
use Symfony\Component\TypeInfo\TypeIdentifier;

class TemplateTypeTest extends TestCase
{
public function testAccepts()
{
$this->assertFalse((new TemplateType('T', new BuiltinType(TypeIdentifier::BOOL)))->accepts('string'));
$this->assertTrue((new TemplateType('T', new BuiltinType(TypeIdentifier::BOOL)))->accepts(true));
}
}
9 changes: 9 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,13 @@ public function testToString()
$type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::object(\DateTime::class), Type::object(\Iterator::class)));
$this->assertSame(\sprintf('(%s&%s)|int|string', \DateTime::class, \Iterator::class), (string) $type);
}

public function testAccepts()
{
$type = new UnionType(Type::int(), Type::bool());

$this->assertFalse($type->accepts('string'));
$this->assertTrue($type->accepts(123));
$this->assertTrue($type->accepts(false));
}
}
20 changes: 20 additions & 0 deletions src/Symfony/Component/TypeInfo/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,24 @@ public function isNullable(): bool
{
return false;
}

/**
* Tells if the type (or one of its wrapped/composed parts) accepts the given $value.
*/
public function accepts(mixed $value): bool
{
$specification = static function (Type $type) use (&$specification, $value): bool {
if ($type instanceof WrappingTypeInterface) {
return $type->wrappedTypeIsSatisfiedBy($specification);
}

if ($type instanceof CompositeTypeInterface) {
return $type->composedTypesAreSatisfiedBy($specification);
}

return $type->accepts($value);
};

return $this->isSatisfiedBy($specification);
}
}
20 changes: 20 additions & 0 deletions src/Symfony/Component/TypeInfo/Type/BuiltinType.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ public function isNullable(): bool
return \in_array($this->typeIdentifier, [TypeIdentifier::NULL, TypeIdentifier::MIXED]);
}

public function accepts(mixed $value): bool
{
return match ($this->typeIdentifier) {
TypeIdentifier::ARRAY => \is_array($value),
TypeIdentifier::BOOL => \is_bool($value),
TypeIdentifier::CALLABLE => \is_callable($value),
TypeIdentifier::FALSE => false === $value,
TypeIdentifier::FLOAT => \is_float($value),
TypeIdentifier::INT => \is_int($value),
TypeIdentifier::ITERABLE => is_iterable($value),
TypeIdentifier::MIXED => true,
TypeIdentifier::NULL => null === $value,
TypeIdentifier::OBJECT => \is_object($value),
TypeIdentifier::RESOURCE => \is_resource($value),
TypeIdentifier::STRING => \is_string($value),
TypeIdentifier::TRUE => true === $value,
default => false,
};
}

public function __toString(): string
{
return $this->typeIdentifier->value;
Expand Down
25 changes: 25 additions & 0 deletions src/Symfony/Component/TypeInfo/Type/CollectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ public function wrappedTypeIsSatisfiedBy(callable $specification): bool
return $this->getWrappedType()->isSatisfiedBy($specification);
}

public function accepts(mixed $value): bool
{
if (!parent::accepts($value)) {
return false;
}

if ($this->isList() && (!\is_array($value) || !array_is_list($value))) {
return false;
}

$keyType = $this->getCollectionKeyType();
$valueType = $this->getCollectionValueType();

if (is_iterable($value)) {
foreach ($value as $k => $v) {
// key or value do not match
if (!$keyType->accepts($k) || !$valueType->accepts($v)) {
return false;
}
}
}

return true;
}

public function __toString(): string
{
return (string) $this->type;
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/TypeInfo/Type/NullableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ public function isNullable(): bool
{
return true;
}

public function accepts(mixed $value): bool
{
return null === $value || parent::accepts($value);
}
}
5 changes: 5 additions & 0 deletions src/Symfony/Component/TypeInfo/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public function isIdentifiedBy(TypeIdentifier|string ...$identifiers): bool
return false;
}

public function accepts(mixed $value): bool
{
return $value instanceof $this->className;
}

public function __toString(): string
{
return $this->className;
Expand Down