Skip to content
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

[WIP] Introduce type classes and property predicates #3

Closed
wants to merge 1 commit into from
Closed
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
15 changes: 10 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@
"src/Lens/properties.php",
"src/Iso/compose.php",
"src/Iso/object_data.php",
"src/Reflect/Predicate/class_has_attribute_of_type.php",
"src/Reflect/Predicate/class_is_dynamic.php",
"src/Reflect/Predicate/property_visibility.php",
"src/Reflect/class_attributes.php",
"src/Reflect/class_has_attribute.php",
"src/Reflect/class_is_dynamic.php",
"src/Reflect/class_info.php",
"src/Reflect/class_properties.php",
"src/Reflect/instantiate.php",
"src/Reflect/object_attributes.php",
"src/Reflect/object_has_attribute.php",
"src/Reflect/object_is_dynamic.php",
"src/Reflect/object_info.php",
"src/Reflect/object_properties.php",
"src/Reflect/properties_get.php",
"src/Reflect/properties_set.php",
"src/Reflect/property_get.php",
"src/Reflect/property_set.php",
"src/Reflect/Internal/reflect_class.php",
"src/Reflect/Internal/reflect_class_attributes.php",
"src/Reflect/Internal/reflect_object.php",
"src/Reflect/Internal/reflect_property.php"
"src/Reflect/Internal/reflect_property.php",
"src/Reflect/Internal/reflect_properties.php"
]
},
"autoload-dev": {
Expand Down
2 changes: 1 addition & 1 deletion src/ArrayAccess/index_get.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*
* @throws ArrayAccessException
*/
function index_get(array $array, $index): mixed
function index_get(array $array, string|int $index): mixed
{
if (!array_key_exists($index, $array)) {
throw ArrayAccessException::cannotAccessIndex($index);
Expand Down
2 changes: 1 addition & 1 deletion src/ArrayAccess/index_set.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*
* @return array<Tk, Tv>
*/
function index_set(array $array, $index, $value): array
function index_set(array $array, string|int $index, $value): array
{
$new = array_merge($array, []);
$new[$index] = $value;
Expand Down
3 changes: 3 additions & 0 deletions src/Exception/CloneException.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

final class CloneException extends RuntimeException
{
/**
* @psalm-param mixed $object
*/
public static function impossibleToClone(mixed $object, ?Throwable $previous = null): self
{
return new self(
Expand Down
14 changes: 9 additions & 5 deletions src/Iso/object_data.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,30 @@
/**
* @template S of object
* @template A of array<string, mixed>
*
* @param class-string<S> $className
* @param null|Lens<S, A> $accessor
*
* @return Iso<S, A>
*
* @psalm-pure
*/
function object_data(string $className): Iso
function object_data(string $className, Lens $accessor = null): Iso
{
/** @var Lens<S, A> $propertiesLens */
$propertiesLens = properties();
/** @var Lens<S, A> $typedAccessor */
$typedAccessor = $accessor ?? properties();

return new Iso(
/**
* @param S $object
* @return A
*/
static fn (object $object): array => $propertiesLens->get($object),
static fn (object $object): array => $typedAccessor->get($object),
/**
* @param A $properties
* @return S
*/
static fn (array $properties): object => $propertiesLens->set(
static fn (array $properties): object => $typedAccessor->set(
instantiate($className),
$properties
),
Expand Down
2 changes: 1 addition & 1 deletion src/Lens/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* @return Lens<S, A>
* @psalm-pure
*/
function index($index): Lens
function index(string|int $index): Lens
{
/** @return Lens<S, A> */
return new Lens(
Expand Down
10 changes: 7 additions & 3 deletions src/Lens/properties.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

namespace VeeWee\Reflecta\Lens;

use Closure;
use VeeWee\Reflecta\Reflect\Type\Property;
use function VeeWee\Reflecta\Reflect\properties_get;
use function VeeWee\Reflecta\Reflect\properties_set;

/**
* @template S of object
* @template A of array<string, mixed>
*
* @param null|Closure(Property): bool $predicate
*
* @return Lens<S, A>
* @psalm-pure
*/
function properties(): Lens
function properties(Closure|null $predicate = null): Lens
{
/** @var Lens<S, A> */
return new Lens(
Expand All @@ -22,12 +26,12 @@ function properties(): Lens
*
* @psalm-suppress InvalidReturnType, InvalidReturnStatement
*/
static fn (object $subject): array => properties_get($subject),
static fn (object $subject): array => properties_get($subject, $predicate),
/**
* @param S $subject
* @param A $value
* @return S
*/
static fn (object $subject, array $value): object => properties_set($subject, $value),
static fn (object $subject, array $value): object => properties_set($subject, $value, $predicate),
);
}
2 changes: 1 addition & 1 deletion src/Psalm/Reflect/Infer/PropertyValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;
use function VeeWee\Reflecta\Reflect\reflect_property;
use function VeeWee\Reflecta\Reflect\Internal\reflect_property;

final class PropertyValueType
{
Expand Down
3 changes: 2 additions & 1 deletion src/Reflect/Internal/reflect_class.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php declare(strict_types=1);
namespace VeeWee\Reflecta\Reflect;

namespace VeeWee\Reflecta\Reflect\Internal;

use ReflectionClass;
use Throwable;
Expand Down
37 changes: 37 additions & 0 deletions src/Reflect/Internal/reflect_class_attributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect\Internal;

use ReflectionAttribute;
use Throwable;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;
use function Psl\Result\wrap;
use function Psl\Vec\map;

/**
* @psalm-internal VeeWee\Reflecta
*
* @template T extends object
*
* @param object|class-string $objectOrClassName
* @param class-string<T>|null $attributeClassName
*
* @return (T is null ? list<object> : list<T>)
* @throws UnreflectableException
*/
function reflect_class_attributes(mixed $objectOrClassName, ?string $attributeClassName = null): array
{
$reflection = is_string($objectOrClassName) ? reflect_class($objectOrClassName) : reflect_object($objectOrClassName);

return map(
$reflection->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF),
static fn (ReflectionAttribute $attribute): object => wrap(static fn () => $attribute->newInstance())
->catch(
static fn (Throwable $error) => throw UnreflectableException::nonInstantiatable(
$attribute->getName(),
$error
)
)
->getResult()
);
}
3 changes: 2 additions & 1 deletion src/Reflect/Internal/reflect_object.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php declare(strict_types=1);
namespace VeeWee\Reflecta\Reflect;

namespace VeeWee\Reflecta\Reflect\Internal;

use ReflectionObject;

Expand Down
36 changes: 36 additions & 0 deletions src/Reflect/Internal/reflect_properties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect\Internal;

use Closure;
use ReflectionProperty;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;
use VeeWee\Reflecta\Reflect\Type\Property;
use function Psl\Dict\filter;
use function Psl\Dict\pull;

/**
* @psalm-internal VeeWee\Reflecta
*
* @param object|class-string $objectOrClassName
* @param null|Closure(Property): bool $predicate
*
* @throws UnreflectableException
* @return array<string, Property>
*/
function reflect_properties(mixed $objectOrClassName, Closure|null $predicate = null): array
{
$reflection = is_string($objectOrClassName) ? reflect_class($objectOrClassName) : reflect_object($objectOrClassName);

$properties = pull(
$reflection->getProperties(),
static fn (ReflectionProperty $reflectionProperty): Property => new Property($reflectionProperty),
static fn (ReflectionProperty $reflectionProperty): string => $reflectionProperty->name
);

if ($predicate !== null) {
return filter($properties, $predicate);
}

return $properties;
}
3 changes: 2 additions & 1 deletion src/Reflect/Internal/reflect_property.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php declare(strict_types=1);
namespace VeeWee\Reflecta\Reflect;

namespace VeeWee\Reflecta\Reflect\Internal;

use ReflectionProperty;
use Throwable;
Expand Down
21 changes: 21 additions & 0 deletions src/Reflect/Predicate/class_has_attribute_of_type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect\Predicate;

use Closure;
use ReflectionAttribute;
use VeeWee\Reflecta\Reflect\Type\ClassInfo;
use function VeeWee\Reflecta\Reflect\Internal\reflect_class;

/**
* @param class-string $attributeClassName
* @return Closure(ClassInfo): bool
*/
function class_has_attribute_of_type(string $attributeClassName): Closure
{
return static function (ClassInfo $class) use ($attributeClassName) : bool {
$reflection = reflect_class($class->fullName());

return (bool) $reflection->getAttributes($attributeClassName, ReflectionAttribute::IS_INSTANCEOF);
};
}
25 changes: 25 additions & 0 deletions src/Reflect/Predicate/class_is_dynamic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect\Predicate;

use AllowDynamicProperties;
use Closure;
use VeeWee\Reflecta\Reflect\Type\ClassInfo;

/**
* @return Closure(ClassInfo): bool
*/
function class_is_dynamic(): Closure
{
return static function (ClassInfo $class) : bool {
// Dynamic props is a 80200 feature.
// IN previous versions, all objects are dynamic (without any warning).
if (PHP_VERSION_ID < 80200) {
return true;
}

return $class->check(
class_has_attribute_of_type(AllowDynamicProperties::class)
);
};
}
15 changes: 15 additions & 0 deletions src/Reflect/Predicate/property_visibility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect\Predicate;

use Closure;
use VeeWee\Reflecta\Reflect\Type\Property;
use VeeWee\Reflecta\Reflect\Type\Visibility;

/**
* @return Closure(Property): bool
*/
function property_visibility(Visibility $visibility): Closure
{
return static fn (Property $property): bool => $property->visibility() === $visibility;
}
77 changes: 77 additions & 0 deletions src/Reflect/Type/ClassInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\Reflect\Type;

use Closure;
use ReflectionClass;

final class ClassInfo
{
public function __construct(
private readonly ReflectionClass $class
) {
}

/**
* @return class-string
*/
public function fullName(): string
{
return $this->class->getName();
}

public function shortName(): string
{
return $this->class->getShortName();
}

public function namespaceName(): string
{
return $this->class->getNamespaceName();
}

/**
* @param Closure(ClassInfo): bool $predicate
*/
public function check(Closure $predicate): bool
{
return $predicate($this);
}

public function isFinal(): bool
{
return $this->class->isFinal();
}

public function isReadOnly(): bool
{
// Readonly classes is a PHP 8.2 feature.
// In previous versions, all objects are not readonly
if (PHP_VERSION_ID < 80_20_0 || !method_exists($this->class, 'isReadOnly')) {
return false;
}

return (bool) $this->class->isReadOnly();
}

public function isAbstract(): bool
{
return $this->class->isAbstract();
}

public function isInstantiable(): bool
{
return $this->class->isInstantiable();
}

public function isCloneable(): bool
{
return $this->class->isCloneable();
}

public function docComment(): string
{
return $this->class->getDocComment();
}
}
Loading
Loading