Skip to content
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
2 changes: 2 additions & 0 deletions src/ScrambleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use Dedoc\Scramble\Support\InferExtensions\ModelExtension;
use Dedoc\Scramble\Support\InferExtensions\PaginateMethodsReturnTypeExtension;
use Dedoc\Scramble\Support\InferExtensions\PossibleExceptionInfer;
use Dedoc\Scramble\Support\InferExtensions\PropertyTypesFromPhpDoc\PropertyTypesFromPhpdocExtension;
use Dedoc\Scramble\Support\InferExtensions\RequestExtension;
use Dedoc\Scramble\Support\InferExtensions\ResourceCollectionTypeInfer;
use Dedoc\Scramble\Support\InferExtensions\ResourceResponseMethodReturnTypeExtension;
Expand Down Expand Up @@ -146,6 +147,7 @@ functions: [
JsonResourceExtension::class,
ResourceResponseMethodReturnTypeExtension::class,
JsonResponseMethodReturnTypeExtension::class,
PropertyTypesFromPhpdocExtension::class,
ModelExtension::class,
EloquentBuilderExtension::class,
RequestExtension::class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Dedoc\Scramble\Support\InferExtensions\PropertyTypesFromPhpDoc;

use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
use Dedoc\Scramble\Support\PhpDoc;
use Illuminate\Support\Collection;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use ReflectionClass;

/**
* Extracts properties from `property` and `property-read` tags on the class docblock.
*/
final class ClassPropertyExtractor implements PropertyExtractor
{
/**
* Retrieve the properties.
*
* @param \ReflectionClass<object> $reflection
* @return \Illuminate\Support\Collection<string, \Dedoc\Scramble\Support\Type\Type>
*/
public function __invoke(ReflectionClass $reflection): Collection
{
$comment = $reflection->getDocComment();

if (! $comment) {
return new Collection;
}

return (new Collection(PhpDoc::parse($comment)->children))
->filter($this->isPropertyTag(...))
->mapWithKeys(function (PhpDocTagNode $tag) {
/** @var \PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode $value */
$value = $tag->value;

$name = ltrim($value->propertyName, '$');

return [$name => PhpDocTypeHelper::toType($value->type)];
});
}

private function isPropertyTag(mixed $tag): bool
{
return $tag instanceof PhpDocTagNode
&& ($tag->name === '@property' || $tag->name === '@property-read')
&& $tag->value instanceof PropertyTagValueNode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Dedoc\Scramble\Support\InferExtensions\PropertyTypesFromPhpDoc;

use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
use Dedoc\Scramble\Support\PhpDoc;
use Illuminate\Support\Collection;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use ReflectionClass;
use ReflectionParameter;
use Webmozart\Assert\Assert;

/**
* Extracts properties from `param` tags on the constructor's docblock for promoted parameters.
*/
final class PromotedParamExtractor implements PropertyExtractor
{
/**
* Retrieve the properties.
*
* @param \ReflectionClass<object> $reflection
* @return \Illuminate\Support\Collection<string, \Dedoc\Scramble\Support\Type\Type>
*/
public function __invoke(ReflectionClass $reflection): Collection
{
$constructor = $reflection->getConstructor();
$phpdoc = $constructor?->getDocComment();

if (! is_string($phpdoc) || $constructor === null) {
return new Collection;
}

$promotedParameters = (new Collection($constructor->getParameters()))
->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted());

return (new Collection(PhpDoc::parse($phpdoc)->children))
->filter(fn (mixed $tag) => $this->isPromotedParamTag($tag, $promotedParameters))
->mapWithKeys(function (PhpDocTagNode $tag) {
Assert::isInstanceOf($tag->value, ParamTagValueNode::class);

$name = ltrim($tag->value->parameterName, '$');

return [$name => PhpDocTypeHelper::toType($tag->value->type)];
});
}

/**
* Check if the tag is a valid `param` tag for a promoted parameter.
*
* @param \Illuminate\Support\Collection<int, \ReflectionParameter> $promotedParameters
*/
private function isPromotedParamTag(mixed $tag, Collection $promotedParameters): bool
{
if (! $tag instanceof PhpDocTagNode || $tag->name !== '@param' || ! $tag->value instanceof ParamTagValueNode) {
return false;
}

$name = ltrim($tag->value->parameterName, '$');

return $promotedParameters->contains(
fn (ReflectionParameter $parameter) => $parameter->getName() === $name,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Dedoc\Scramble\Support\InferExtensions\PropertyTypesFromPhpDoc;

use Illuminate\Support\Collection;
use ReflectionClass;

interface PropertyExtractor
{
/**
* Retrieve the properties.
*
* @param \ReflectionClass<object> $reflection
* @return \Illuminate\Support\Collection<string, \Dedoc\Scramble\Support\Type\Type>
*/
public function __invoke(ReflectionClass $reflection): Collection;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Dedoc\Scramble\Support\InferExtensions\PropertyTypesFromPhpDoc;

use Dedoc\Scramble\Infer\Extensions\Event\PropertyFetchEvent;
use Dedoc\Scramble\Infer\Extensions\PropertyTypeExtension;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionException;

final class PropertyTypesFromPhpdocExtension implements PropertyTypeExtension
{
/**
* @var array<string, \Illuminate\Support\Collection<string, \Dedoc\Scramble\Support\Type\Type>>
*/
public static array $cache = [];

/**
* Determine whether the extension should handle the given type.
*/
public function shouldHandle(ObjectType $type): bool
{
return true;
}

/**
* Get the property type from the given event.
*/
public function getPropertyType(PropertyFetchEvent $event): ?Type
{
$type = $event->getInstance();

$properties = $this->getProperties($type);

dump($properties);

return $properties->get($event->name);
}

/**
* @return \Illuminate\Support\Collection<string, \Dedoc\Scramble\Support\Type\Type>
*/
private function getProperties(ObjectType $type): Collection
{
if (isset(self::$cache[$type->name])) {
return self::$cache[$type->name];
}

$name = $type->name;

try {
$reflection = new ReflectionClass($name);
} catch (ReflectionException $e) {
return new Collection;
}

$extractors = [
new PromotedParamExtractor,
new VarPropertyExtractor,
new ClassPropertyExtractor,
];

/** @var \Illuminate\Support\Collection<string, \Dedoc\Scramble\Support\Type\Type> $properties */
$properties = new Collection;

foreach ($extractors as $extractor) {
$properties = $properties->merge($extractor($reflection));
}

return self::$cache[$type->name] = $properties;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Dedoc\Scramble\Support\InferExtensions\PropertyTypesFromPhpDoc;

use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
use Dedoc\Scramble\Support\PhpDoc;
use Illuminate\Support\Collection;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use ReflectionClass;
use Webmozart\Assert\Assert;

/**
* Extracts properties from `var` tags on the properties of a class.
*/
final class VarPropertyExtractor implements PropertyExtractor
{
/**
* Retrieve the properties.
*
* @param \ReflectionClass<object> $reflection
* @return \Illuminate\Support\Collection<string, \Dedoc\Scramble\Support\Type\Type>
*/
public function __invoke(ReflectionClass $reflection): Collection
{
$properties = new Collection;

foreach ($reflection->getProperties() as $reflectionProperty) {
$phpDoc = $reflectionProperty->getDocComment();

if (! is_string($phpDoc)) {
continue;
}

$property = new Collection(PhpDoc::parse($phpDoc)->children)
->filter($this->isVarTag(...))
->map(function (PhpDocTagNode $tag) {
Assert::isInstanceOf($tag->value, VarTagValueNode::class);

return PhpDocTypeHelper::toType($tag->value->type);
})
->first();

if ($property !== null) {
$properties->put($reflectionProperty->getName(), $property);
}
}

return $properties;
}

/**
* Check if the tag is a valid `var` tag.
*/
private function isVarTag(mixed $tag): bool
{
return $tag instanceof PhpDocTagNode
&& $tag->name === '@var'
&& $tag->value instanceof VarTagValueNode;
}
}
1 change: 1 addition & 0 deletions tests/InferExtensions/ModelExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
new PropertyFetchReferenceType($object, $name)
);

// TODO: Fails due to role model having invalid PHPDoc, `?\Illuminate\Support\Carbon` should be `\Illuminate\Support\Carbon|null`
expect(Str::replace('Dedoc\\Scramble\\Tests\\Files\\', '', $propertyType->toString()))
->toBe($type);
}
Expand Down
Loading