-
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from Crell/func-analyzer
Add a function/closure analyzer
- Loading branch information
Showing
24 changed files
with
563 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Crell\AttributeUtils; | ||
|
||
/** | ||
* Marks a function-targeting attribute as wanting reflection information. | ||
* | ||
* If a function-targeting attribute implements this interface, then after it | ||
* is instantiated the reflection object for the function will be passed to this | ||
* method. The attribute may then extract whatever information it desires | ||
* and save it to object however it likes. | ||
* | ||
* Note that the attribute MUST NOT save the reflection object itself. That | ||
* would make the attribute object unserializable, and thus uncacheable. | ||
*/ | ||
interface FromReflectionFunction | ||
{ | ||
public function fromReflection(\ReflectionFunction $subject): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<?php | ||
|
||
namespace Crell\AttributeUtils; | ||
|
||
class FuncAnalyzer implements FunctionAnalyzer | ||
{ | ||
public function analyze(string|\Closure $function, string $attribute, array $scopes = []): object | ||
{ | ||
$parser = new AttributeParser($scopes); | ||
$defBuilder = new ReflectionDefinitionBuilder($parser); | ||
|
||
try { | ||
$subject = new \ReflectionFunction($function); | ||
|
||
$funcDef = $parser->getAttribute($subject, $attribute) ?? new $attribute; | ||
|
||
if ($funcDef instanceof FromReflectionFunction) { | ||
$funcDef->fromReflection($subject); | ||
} | ||
|
||
$defBuilder->loadSubAttributes($funcDef, $subject); | ||
|
||
if ($funcDef instanceof ParseParameters) { | ||
$parameters = $defBuilder->getDefinitions( | ||
$subject->getParameters(), | ||
fn (\ReflectionParameter $p) | ||
=> $defBuilder->getComponentDefinition($p, $funcDef->parameterAttribute(), $funcDef->includeParametersByDefault(), FromReflectionParameter::class, $funcDef) | ||
); | ||
$funcDef->setParameters($parameters); | ||
} | ||
|
||
if ($funcDef instanceof Finalizable) { | ||
$funcDef->finalize(); | ||
} | ||
|
||
return $funcDef; | ||
} catch (\ArgumentCountError $e) { | ||
$this->translateArgumentCountError($e); | ||
} | ||
} | ||
|
||
/** | ||
* Throws a domain-specific exception based on an ArgumentCountError. | ||
* | ||
* This is absolutely hideous, but this is what happens when your throwable | ||
* puts all the useful information in the message text rather than as useful | ||
* properties or methods or something. | ||
* | ||
* Conclusion: Write better, more debuggable exceptions than PHP does. | ||
*/ | ||
protected function translateArgumentCountError(\ArgumentCountError $error): never | ||
{ | ||
$message = $error->getMessage(); | ||
// PHPStan doesn't understand this syntax style of sscanf(), so skip it. | ||
// @phpstan-ignore-next-line | ||
[$classAndMethod, $passedCount, $file, $line, $expectedCount] = sscanf( | ||
string: $message, | ||
format: "Too few arguments to function %s::%s, %d passed in %s on line %d and exactly %d expected" | ||
); | ||
[$className, $methodName] = \explode('::', $classAndMethod ?? ''); | ||
|
||
throw RequiredAttributeArgumentsMissing::create($className, $error); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
namespace Crell\AttributeUtils; | ||
|
||
interface FunctionAnalyzer | ||
{ | ||
/** | ||
* Analyzes a function or closure for the specified attribute. | ||
* | ||
* @template T of object | ||
* @param string|\Closure $function | ||
* Either a fully qualified function name or a Closure to analyze. | ||
* @param class-string<T> $attribute | ||
* The fully qualified class name of the class attribute to analyze. | ||
* @param array<string|null> $scopes | ||
* The scopes for which this analysis should run. | ||
* @return T | ||
* The function attribute requested, including dependent data as appropriate. | ||
*/ | ||
public function analyze(string|\Closure $function, string $attribute, array $scopes = []): object; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Crell\AttributeUtils; | ||
|
||
/** | ||
* A simple in-memory cache for function analyzers. | ||
*/ | ||
class MemoryCacheFunctionAnalyzer implements FunctionAnalyzer | ||
{ | ||
/** | ||
* @var array<string, array<string, array<string, object>>> | ||
*/ | ||
private array $cache = []; | ||
|
||
public function __construct( | ||
private readonly FunctionAnalyzer $analyzer, | ||
) {} | ||
|
||
public function analyze(string|\Closure $function, string $attribute, array $scopes = []): object | ||
{ | ||
// We cannot cache a closure, as we have no reliable identifier for it. | ||
if ($function instanceof \Closure) { | ||
return $this->analyzer->analyze($function, $attribute, $scopes); | ||
} | ||
|
||
$scopekey = ''; | ||
if ($scopes) { | ||
sort($scopes); | ||
$scopekey = implode(',', $scopes); | ||
} | ||
|
||
return $this->cache[$function][$attribute][$scopekey] ??= $this->analyzer->analyze($function, $attribute, $scopes); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Crell\AttributeUtils; | ||
|
||
use Psr\Cache\CacheItemPoolInterface; | ||
|
||
class Psr6FunctionCacheAnalyzer implements FunctionAnalyzer | ||
{ | ||
public function __construct( | ||
private readonly FunctionAnalyzer $analyzer, | ||
private readonly CacheItemPoolInterface $pool, | ||
) {} | ||
|
||
public function analyze(\Closure|string $function, string $attribute, array $scopes = []): object | ||
{ | ||
// We cannot cache a closure, as we have no reliable identifier for it. | ||
if ($function instanceof \Closure) { | ||
return $this->analyzer->analyze($function, $attribute, $scopes); | ||
} | ||
|
||
$key = $this->buildKey($function, $attribute, $scopes); | ||
|
||
$item = $this->pool->getItem($key); | ||
if ($item->isHit()) { | ||
return $item->get(); | ||
} | ||
|
||
// No expiration; the cached data would only need to change | ||
// if the source code changes. | ||
$value = $this->analyzer->analyze($function, $attribute, $scopes); | ||
$item->set($value); | ||
$this->pool->save($item); | ||
return $value; | ||
} | ||
|
||
/** | ||
* Generates the cache key for this request. | ||
* | ||
* @param array<string|null> $scopes | ||
* The scopes for which this analysis should run. | ||
*/ | ||
private function buildKey(string $function, string $attribute, array $scopes): string | ||
{ | ||
$parts = [ | ||
$function, | ||
$attribute, | ||
implode(',', $scopes), | ||
]; | ||
|
||
return str_replace('\\', '_', \implode('-', $parts)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
namespace Crell\AttributeUtils\Attributes\Functions; | ||
|
||
use Crell\AttributeUtils\ParseParameters; | ||
|
||
#[\Attribute(\Attribute::TARGET_FUNCTION)] | ||
class HasParameters implements ParseParameters | ||
{ | ||
public readonly array $parameters; | ||
|
||
public function __construct( | ||
public readonly string $parameter, | ||
public readonly bool $parseParametersByDefault = true) {} | ||
|
||
public function setParameters(array $parameters): void | ||
{ | ||
$this->parameters = $parameters; | ||
} | ||
|
||
public function includeParametersByDefault(): bool | ||
{ | ||
return $this->parseParametersByDefault; | ||
} | ||
|
||
public function parameterAttribute(): string | ||
{ | ||
return $this->parameter; | ||
} | ||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace Crell\AttributeUtils\Attributes\Functions; | ||
|
||
use Crell\AttributeUtils\FromReflectionFunction; | ||
|
||
#[\Attribute(\Attribute::TARGET_FUNCTION)] | ||
class IncludesReflection implements FromReflectionFunction | ||
{ | ||
public readonly string $name; | ||
|
||
public function fromReflection(\ReflectionFunction $subject): void | ||
{ | ||
$this->name = $subject->name; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?php | ||
|
||
namespace Crell\AttributeUtils\Attributes\Functions; | ||
|
||
#[\Attribute(\Attribute::TARGET_PARAMETER)] | ||
class ParameterAttrib | ||
{ | ||
public function __construct(public readonly string $a = 'default') {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?php | ||
|
||
namespace Crell\AttributeUtils\Attributes\Functions; | ||
|
||
#[\Attribute(\Attribute::TARGET_FUNCTION)] | ||
class RequiredArg | ||
{ | ||
public function __construct(public readonly string $a) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?php | ||
|
||
namespace Crell\AttributeUtils\Attributes\Functions; | ||
|
||
#[\Attribute(\Attribute::TARGET_FUNCTION)] | ||
class SubChild | ||
{ | ||
public function __construct(public string $b = 'default') {} | ||
} |
Oops, something went wrong.