Compare commits

...

35 Commits

Author SHA1 Message Date
Dave Mc Nicoll
0db34fe228 - Added old migration script, reworked security attribute also 2025-05-23 19:03:39 +00:00
Dave Mc Nicoll
fa4fe32277 - Small fix on reflection property name 2025-05-19 17:31:33 +00:00
Dave Mc Nicoll
e20157a162 - Unauthorize response now allows ResponseInterface as type 2025-04-01 19:16:30 +00:00
63c7dc23f9 - Reworked url default value removal; preg_replace will remove them without having to slit the string 2024-11-22 20:49:51 -05:00
a4d99dfe08 - Route now supports default values 2024-11-22 20:46:23 -05:00
Dave Mc Nicoll
f199cc7085 - Merged v2.0.0b branch into master; rewrite is officially stable and a drop-in replacement of v1.x 2024-11-13 14:00:50 -05:00
Dave Mc Nicoll
08c0ffacab Merge branch 'v2.0.0b' of https://git.mcnd.ca/mcndave/notes into v2.0.0b 2024-10-29 16:00:36 +00:00
Dave Mc Nicoll
6ba66dcd37 - Adjusted for notes V2.x 2024-10-29 16:00:12 +00:00
Dave Mc Nicoll
81fa3d49c7 - Breadcrumb now shows up only if deep within at least two routes (if ignoreSoleRoute is true) 2024-10-14 12:52:34 +00:00
Dave Mc Nicoll
fcb332ae53 Merge branch 'master' of https://git.mcnd.ca/mcndave/notes 2024-10-07 15:44:24 -04:00
Dave Mc Nicoll
133672b347 - WIP on Breadcrumb, AnnotationReader now return #UNKNOWN# if reflection fail 2024-10-07 15:44:00 -04:00
Dave Mc Nicoll
34a3351ed5 - Added classReflection property 2024-06-21 14:17:57 +00:00
Dave Mc Nicoll
dd680c8a9a v2.0.0b 2024-06-04 13:33:11 +00:00
Dave Mc Nicoll
c92baf6cda - Removed NULL and added a new Enum instead 2024-06-02 23:02:27 +00:00
Dave Mc Nicoll
86afea9b4e - Reworked the SecurityHandler granting part which now supports NULL as a 'skipping' value. This necessity appeared since a new property in the attribute was added. 2024-06-02 19:54:55 +00:00
Dave Mc Nicoll
4568427c66 - Added HTTP methods to Taxus object 2024-05-31 19:00:32 +00:00
Dave Mc Nicoll
19c79d0374 - getAttribute() can now query multiple types 2024-05-31 12:28:00 +00:00
Dave Mc Nicoll
0cdc2dc3d8 - Added some more method to filter properties 2024-05-27 18:06:56 +00:00
Dave Mc Nicoll
2a6ec3cd1b Merge branch 'master' into v2.0.0b 2024-05-21 13:03:31 +00:00
Dave Mc Nicoll
1599284334 - Added route description 2024-05-21 13:03:18 +00:00
2345c5453f - Added MatchRoute Method 2024-05-09 19:52:10 +00:00
61b84bfd09 - Totally reworked Notes to allievate most conceptions problems. Removed old annotations artifacts too 2024-05-09 19:43:08 +00:00
Dave Mc Nicoll
996c8fa14a - Fixed Notes bugs with Union Types Reflection 2024-04-24 11:24:27 -04:00
340235e2f8 - Base can now be null 2023-11-20 13:40:21 -05:00
124bddb238 - Latest work on route base fallbacks 2023-11-17 22:38:26 -05:00
0fcff83eac - Fixed missing tags from traits 2023-11-17 21:59:29 -05:00
5f00c2b603 - Fixed a typo 2023-11-17 19:57:47 +00:00
cee97fca25 - Merged multiple sub-libraries 2023-11-17 19:47:24 +00:00
767152e0c3 - Removing annotations replaced by attributes 2023-11-03 19:54:44 -04:00
dd0f0271a6 - Some bugfixes linked to attributes 2023-07-09 12:41:36 -04:00
Dave Mc Nicoll
7c7d8c9d8d - Set # migrated from: optional from migrate script 2023-03-01 11:33:33 -05:00
Dave Mc Nicoll
e3e922e3bf - Added a cleanup method to the migration process 2023-02-23 09:36:54 -05:00
Dave Mc Nicoll
564a05a1d4 Merge branch 'master' of https://git.mcnd.ca/mcndave/notes into attributes 2023-02-08 16:32:11 +00:00
Dave Mc Nicoll
1accac94c0 - Fixed the Ignore attribute 2023-02-08 16:31:51 +00:00
aca093dd97 - Fixed a bug with Union types within methods arguments 2023-02-07 16:50:16 +00:00
34 changed files with 1493 additions and 571 deletions

View File

@ -1,120 +0,0 @@
#!/usr/bin/php
<?php
array_shift($argv);
$opt = getopt('c::f:v', [ 'folder:', 'confirm', 'verbose' ]);
if ( ! $opt ) {
exit("Bad arguments provided, you must specify a fullpath using the -f or --folder option\n");
}
$iterator = new RecursiveIteratorIterator(new class(new RecursiveDirectoryIterator($opt['folder'] ?? $opt['f'], FilesystemIterator::SKIP_DOTS)) extends RecursiveFilterIterator {
public function accept() : bool {
return true;
}
}, RecursiveIteratorIterator::SELF_FIRST);
$files = array();
foreach ($iterator as $info) {
if ($info->getExtension() !== "php" || $info->isDir() ) {
continue;
}
echo sprintf("\n### MIGRATING FILE %s\n", $info->getFilename());
$opened = false;
$attr = $newContent = [];
$content = file_get_contents($info->getPathname());
foreach(explode(PHP_EOL, $content) as $lineIdx => $line) {
$tline = trim($line);
if ($tline === "/**") {
$opened = true;
}
elseif ($tline === '*/') {
$opened = false;
}
elseif (substr($tline, 0, 1) === '*') {
$annotation = trim(substr($tline, 1));
if (strpos($annotation, '@') === 0 ) {
$argpos = strpos($annotation, '(');
if ($argpos !== false) {
$name = substr($annotation, 1, $argpos - 1);
$args = substr($annotation, $argpos + 1, strrpos($annotation, ')') - $argpos - 1);
try{
$array = eval("return [{$args}];");
$renderedArgs = [];
foreach($array as $key => $argument) {
if ( is_array($argument) ) {
$argument = var_export($argument, true);
}
elseif ( is_bool($argument) ) {
$argument = $argument ? 'true' : 'false';
}
elseif (! is_numeric($argument) ) {
$argument = "\"$argument\"";
}
if (is_numeric($key)) {
$renderedArgs[] = $argument;
}
else {
$renderedArgs[] = "$key: $argument";
}
}
$renderedArgs = implode(', ', $renderedArgs);
}
catch(\Throwable $e) {
echo sprintf("!!! WARNING : %s in file (%s) line %d ; using previous notation.\n", $e->getMessage(), $info->getPathname(), $lineIdx + 1);
$renderedArgs = str_replace(' => ', ': ', $args);
}
}
else {
$name = substr($annotation, 1);
$renderedArgs = "";
}
$attr[] = $attribute = sprintf("%s#[%s%s]%s", str_repeat(" ", strlen($line) - strlen(ltrim($line)) >= 4 ? 4 : 0), $name, $renderedArgs ? "($renderedArgs)" : "", $renderedArgs ? " # migrated from: $args" : "");
$newContent[] = $attribute;
}
}
else {
$newContent[] = $line;
}
}
$newContent = implode(PHP_EOL, $newContent);
if ($attr) {
if ( isset($opt['verbose']) || isset($opt['v']) ) {
echo $newContent;
}
if ( isset($opt['confirm']) || isset($opt['c']) ) {
file_put_contents($info->getPathname(), $newContent);
echo sprintf("\n### FILE CONVERSION COMPLETED %s\n", $info->getFilename());
}
else {
echo sprintf("\n### FILE CONVERSION ANALYZED, NOTHING WRITTEN UNTIL --confirm or -c FLAG IS PROVIDED %s\n", $info->getFilename());
}
}
else {
echo sprintf("\n### NOTHING TO DO ON FILE %s\n", $info->getFilename());
}
}

View File

@ -6,10 +6,12 @@
"authors": [ "authors": [
{ {
"name": "Dave Mc Nicoll", "name": "Dave Mc Nicoll",
"email": "mcndave@gmail.com" "email": "info@mcnd.ca"
} }
], ],
"require": {}, "require": {
"mcnd/kash" : "dev-master"
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Notes\\": "src/" "Notes\\": "src/"

View File

@ -34,12 +34,12 @@ class AnnotationReader
return $this->parseDocComment($method); return $this->parseDocComment($method);
} }
protected function parseDocComment(Reflector $reflect) protected function parseDocComment(\Reflector $reflect)
{ {
$tags = []; $tags = [];
if ( $reflect->getAttributes() ) { if ( $attributes = $reflect->getAttributes() ) {
foreach($reflect->getAttributes() as $attr) { foreach($attributes as $attr) {
try { try {
$tags[] = [ $tags[] = [
'tag' => substr(strrchr($attr->getName(), "\\"), 1), 'tag' => substr(strrchr($attr->getName(), "\\"), 1),
@ -99,6 +99,8 @@ class AnnotationReader
case $reflect instanceof ReflectionClass : case $reflect instanceof ReflectionClass :
return $reflect->name; return $reflect->name;
} }
return "#UNKNOWN#";
} }
protected function getObjectNamespace(Reflector $reflect) : string protected function getObjectNamespace(Reflector $reflect) : string

6
src/Attribute/Ignore.php Normal file
View File

@ -0,0 +1,6 @@
<?php
namespace Notes\Attribute;
#[\Attribute(\Attribute::TARGET_ALL)]
class Ignore implements \Notes\Attribute {}

60
src/AttributeReader.php Normal file
View File

@ -0,0 +1,60 @@
<?php
namespace Notes;
use Notes\Common\ReflectedAttribute;
use Reflector, ReflectionClass, ReflectionProperty, ReflectionMethod;
class AttributeReader
{
const PHP_TYPES = [ "string", "int", "float", "object", "double", "closure", ];
public static function reflectAttributes(\Reflector $reflect) : array
{
$list = [];
foreach($reflect->getAttributes() as $attribute) {
try {
$list[] = new ReflectedAttribute(
tag: substr(strrchr($attribute->getName(), "\\"), 1),
object: $attribute->newInstance(),
arguments: $attribute->getArguments(),
);
}
catch(\Throwable $e) {
throw new \Exception(sprintf("%s for %s in file '%s'", $e->getMessage(), $reflect, static::getObjectName($reflect)));
}
}
return $list;
}
protected static function getObjectName(Reflector $reflect) : string
{
switch(true) {
case $reflect instanceof ReflectionMethod :
return $reflect->class . "::" . $reflect->name;
case $reflect instanceof ReflectionProperty :
return $reflect->class . "::$" . $reflect->name;
case $reflect instanceof ReflectionClass :
return $reflect->name;
}
throw new \InvalidArgumentException("Given reflector is not supported within getObjectName().");
}
protected static function getObjectNamespace(Reflector $reflect) : string
{
switch(true) {
case $reflect instanceof ReflectionMethod :
case $reflect instanceof ReflectionProperty :
return $reflect->getDeclaringClass()->getNamespaceName();
case $reflect instanceof ReflectionClass :
return $reflect->getNamespaceName();
}
throw new \InvalidArgumentException("Given reflector is not supported within getObjectNamespace().");
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Notes\Breadcrumb\Attribute\Method;
use Notes\Route\Attribute\Method\Route;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)]
class Breadcrumb implements \Notes\Attribute {
public Route $routeAnnotation;
public bool $current = false;
public function __construct(
public string $parent = "",
public string $icon = "",
public string $lang = "",
public string $route = "",
public array $methods = [],
public string $class = "",
public string $classMethod = "",
) {}
}

View File

@ -0,0 +1,75 @@
<?php namespace Notes\Breadcrumb;
use Notes\ObjectReflection,
Notes\ObjectResolver;
use Notes\Route\{ RouteFetcher, Attribute as RouteAttribute };
use Psr\Http\Message\ServerRequestInterface;
use Psr\SimpleCache\CacheInterface;
use RuntimeException, DirectoryIterator, Generator;
class Breadcrumb extends RouteFetcher {
public bool $ignoreSoleRoute = true;
public function getRouteTree(\Notes\Route\Attribute\Method\Route $route) : array
{
$tree = [];
$routes = $this->compile([
'object' => [ RouteAttribute\Object\Route::class ],
'method' => [ RouteAttribute\Method\Route::class ],
]);
$crumbs = $this->compile([
'method' => [ Attribute\Method\Breadcrumb::class ],
]);
while( $route ) {
$crumb = $this->getBreadcrumbAttribute($crumbs, $route);
if ($crumb) {
empty($tree) && $crumb->current = true;
$tree[$route->name] = $crumb;
$route = $this->getRouteAttribute($routes, $crumb);
}
else {
$route = null;
}
}
if ($this->ignoreSoleRoute && count($tree) === 1) {
return [];
}
return array_reverse($tree);
}
protected function getRouteAttribute(array $list, Attribute\Method\Breadcrumb $crumb) : null|RouteAttribute\Method\Route
{
if ($crumb->parent ?? null) {
foreach ($list as $route) {
if ($crumb->parent === $route->name) {
return $route;
}
}
}
return null;
}
protected function getBreadcrumbAttribute(array $list, RouteAttribute\Method\Route $route) : null|Attribute\Method\Breadcrumb
{
foreach($list as $crumb) {
if ($crumb->class === $route->class && $crumb->classMethod === $route->classMethod) {
$crumb->routeAnnotation = $route;
return $crumb;
}
}
return null;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Notes\CLI\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Command implements \Notes\Attribute {
public function __construct(
public string $description,
public null|string $command = null,
public string|\Closure|null $default = null,
public null|string $version = null,
) {}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Notes\CLI\Attribute;
use Attribute;
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class Option extends \Mcnd\CLI\Option implements \Notes\Attribute {
}

136
src/CLI/CommandFetcher.php Normal file
View File

@ -0,0 +1,136 @@
<?php namespace Notes\CLI;
use Kash\HandleCacheTrait;
use Mcnd\CLI\Command;
use Mcnd\CLI\CommandStack;
use Mcnd\CLI\Option;
use Notes\ObjectResolver;
use Psr\SimpleCache\CacheInterface;
use RuntimeException, DirectoryIterator, Generator, Closure;
class CommandFetcher {
use HandleCacheTrait;
protected array $folderList;
protected Closure $callback;
public array $defaultMethods = [ 'GET', 'POST' ];
protected array $annotations;
public function __construct( ?array $folderList = null, ?array $annotations = null, ? CacheInterface $cache = null)
{
$this->cache = $cache;
if ($folderList !== null) {
$this->folderList = $folderList;
}
if ($annotations !== null) {
$this->annotations = $annotations;
}
else {
$this->annotations = [
'object' => [ Attribute\Command::class, Attribute\Option::class ],
'method' => [ Attribute\Command::class, Attribute\Option::class ],
];
}
}
public function addFolder($folder) : void
{
$this->folderList[] = $folder;
}
public function setFolderList(array $list) : void
{
$this->folderList = $list;
}
public function scan(? array $folders = null) : Generator
{
foreach($folders ?: $this->folderList as $namespace => $folder) {
if ( ! file_exists($folder) ) {
throw new RuntimeException(sprintf("Folder `%s` can not be found or scanned", $folder));
}
foreach (new DirectoryIterator($folder) as $fileinfo) {
if ( ! $fileinfo->isDot() ) {
if ( $fileinfo->isDir() ) {
foreach($this->scan([ "{$namespace}\\" . $fileinfo->getBasename() => $fileinfo->getPathname() ]) as $ns2 => $fi2) {
yield $ns2 => $fi2;
}
}
else {
yield $namespace => $fileinfo;
}
}
}
}
}
public function compile() : CommandStack
{
return $this->handleCaching(substr(md5(serialize($this->annotations)), 0, 7), function() : CommandStack {
$stack = new CommandStack();
$classCmd = [];
foreach($this->scan() as $namespace => $file) {
if ( $file->getExtension() !== "php" ) {
continue;
}
$basename = $file->getBasename(".php");
$class = $this->generateClassname($basename, $namespace);
$objectResolver = new ObjectResolver($class, $this->cache);
$attributes = $objectResolver->getAttributeListFromClassname( $this->annotations['object'], false ) ;
$commandList = array_filter($attributes, fn($e) => $e instanceof Attribute\Command);
ksort($commandList);
if ($commandList) {
$fullCommand = [];
# Build full command token
foreach($commandList as $cmd) {
if ($cmd->command) {
$fullCommand[] = $cmd->command;
}
}
$fullCommand = implode(' ', $fullCommand);
$commandHeader = new Command(name: $fullCommand, description: end($commandList)->description);
$commandHeader->pushOption(array_filter($attributes, fn($e) => $e instanceof Attribute\Option));
$stack->append($commandHeader);
foreach($objectResolver->getAttributeListFromClassname( $this->annotations['method'], false ) as $method => $commands) {
if (is_array($commands) ) {
$commandAttribute = array_filter($commands, fn($e) => $e instanceof Attribute\Command)[0] ?? null;
$name = implode(' ', array_filter( [ $fullCommand, $commandAttribute->command ?? $method ]));
$commandFunction = new Command(name: $name, description: $commandAttribute->description, parent: $commandHeader, callback: "$class::$method");
$stack->append($commandFunction);
$commandFunction->pushOption(array_filter($commands, fn($e) => $e instanceof Attribute\Option));
}
}
}
}
return $stack;
});
}
protected function generateClassname($file, $namespace)
{
return "\\$namespace\\$file";
}
}

83
src/Common/Reflected.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace Notes\Common;
use Notes\Attribute\Ignore;
abstract class Reflected
{
public function hasIgnoreAttribute() : bool
{
return [] !== array_filter($this->attributes, fn($e) => $e->object instanceof Ignore);
}
public function allowsNull() : bool
{
return empty($this->type) || $this->expectType("null");
}
public function expectType(string $type) : bool
{
$type = strtolower($type);
foreach($this->getTypes() as $item) {
if ($type === "null") {
if ($item->type === "null" || $item->nullable) {
return true;
}
}
elseif ($type === $item->type) {
return true;
}
}
return false;
}
public function getTypes() : array
{
return is_array($this->type) ? $this->type : [ $this->type ];
}
public function typeFromReflection(\ReflectionProperty|\ReflectionParameter $property) : void
{
if ( $property->hasType() ) {
$type = $property->getType();
if ($type instanceof \ReflectionUnionType ) {
$this->type = [];
foreach($type->getTypes() as $item) {
$this->type[] = new ReflectedPropertyType($item->getName(), $item->isBuiltIn(), $item->allowsNull());
}
}
else {
$this->type = new ReflectedPropertyType($type->getName(), $type->isBuiltIn(), $type->allowsNull());
}
}
}
public function getAttributes(null|string|array $attributeType = null): array
{
if ($attributeType) {
$list = [];
foreach($this->attributes as $attribute) {
foreach((array) $attributeType as $type) {
if ($attribute->object instanceof $type) {
$list[] = $attribute;
}
}
}
return array_reverse($list);
}
return array_reverse($this->attributes);
}
public function getAttribute(string|array $attributeType): ?object
{
return $this->getAttributes($attributeType)[0] ?? null;
}
}

82
src/Common/Reflected.php~ Normal file
View File

@ -0,0 +1,82 @@
<?php
namespace Notes\Common;
use Notes\Attribute\Ignore;
abstract class Reflected
{
public function hasIgnoreAttribute() : bool
{
return [] !== array_filter($this->attributes, fn($e) => $e->object instanceof Ignore);
}
public function allowsNull() : bool
{
# dump($this);
return empty($this->type) || $this->expectType("null");
}
public function expectType(string $type) : bool
{
$type = strtolower($type);
foreach($this->getTypes() as $item) {
if ($type === "null") {
if ($item->type === "null" || $item->nullable) {
return true;
}
}
elseif ($type === $item->type) {
return true;
}
}
return false;
}
public function getTypes() : array
{
return is_array($this->type) ? $this->type : [ $this->type ];
}
public function typeFromReflection(\ReflectionProperty|\ReflectionParameter $property) : void
{
if ( $property->hasType() ) {
$type = $property->getType();
if ($type instanceof \ReflectionUnionType ) {
foreach($type->getTypes() as $item) {
$this->type[] = new ReflectedPropertyType($item->getName(), $item->isBuiltIn(), $item->allowsNull());
}
}
else {
$this->type = new ReflectedPropertyType($type->getName(), $type->isBuiltIn(), $type->allowsNull());
}
}
}
public function getAttributes(null|string|array $attributeType = null): array
{
if ($attributeType) {
$list = [];
foreach($this->attributes as $attribute) {
foreach((array) $attributeType as $type) {
if ($attribute->object instanceof $type) {
$list[] = $attribute;
}
}
}
return array_reverse($list);
}
return array_reverse($this->attributes);
}
public function getAttribute(string|array $attributeType): ?object
{
return $this->getAttributes($attributeType)[0] ?? null;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Notes\Common;
class ReflectedAttribute
{
public function __construct(
public mixed $tag,
public object $object,
public array $arguments = [],
){}
}

View File

@ -0,0 +1,183 @@
<?php
namespace Notes\Common;
class ReflectedClass implements ReflectedInterface
{
public function __construct(
public string $name,
public false|ReflectedClass $parent = false,
public array $methods = [],
public array $properties = [],
public array $attributes = [],
public array $interfaces = [],
public array $traits = [],
) {}
public function getClassName() : string
{
return end(explode('\\', $this->name));
}
public function getProperties(bool $deep = true) : array
{
return $deep ? array_replace(
$this->parent ? $this->parent->getProperties(true) : [],
$this->properties
) : $this->properties;
}
public function getMethods(bool $deep = true) : array
{
$m = array_merge(...array_map(fn($e) => $e->getMethods(true), $this->traits));
return $deep ? array_replace(
$this->parent ? $this->parent->getMethods(true) : [],
array_merge(...array_map(fn($e) => $e->getMethods(true), $this->interfaces)),
array_merge(...array_map(fn($e) => $e->getMethods(true), $this->traits)),
$this->methods
) : $this->methods;
}
public function getAttributes(bool $deep = true, ? string $attributeType = null) : array
{
return array_reverse($this->getOrderedAttributes($deep, $attributeType));
}
protected function getOrderedAttributes(bool $deep = true, ? string $attributeType = null) : array
{
$attributes = $deep ? array_merge(
$this->parent ? $this->parent->getOrderedAttributes(true) : [],
array_merge(...array_map(fn($e) => $e->getOrderedAttributes(true), $this->interfaces)),
array_merge(...array_map(fn($e) => $e->getOrderedAttributes(true), $this->traits)),
$this->attributes
) : $this->attributes;
if ($attributeType) {
$list = [];
foreach($attributes as $attribute) {
if ($attribute->object instanceof $attributeType) {
$list[] = $attribute;
}
}
return $list;
}
return $attributes;
}
# Get Attributes based on Native attributes's target
public function getClassAttributeList(array|string $className, bool $deepSearch, bool $throwOnError = true) : array
{
$list = [];
foreach((array) $className as $class) {
$reflection = new \ReflectionClass($class);
$attributes = $reflection->getAttributes();
foreach($attributes as $attribute) {
# Native PHP Attribute
if ($attribute->getName() === 'Attribute') {
if ($attribute->getTarget() && \Attribute::TARGET_CLASS) {
$list = $this->getClassAttributeListFromClassname($class, $deepSearch, false);
}
if ($attribute->getTarget() && \Attribute::TARGET_METHOD) {
$list = array_merge($list, $this->getMethodAttributeListFromClassname($class, $deepSearch, false));
}
if ($attribute->getTarget() && \Attribute::TARGET_PROPERTY) {
$list = array_merge($list, $this->getPropertyAttributeListFromClassname($class, $deepSearch, false));
}
}
}
}
if ( empty($list) && $throwOnError ) {
$className = implode(',', (array) $className);
throw new \InvalidArgumentException("Attribute(s) `$className` was not found within class {$this->name} (or it's parents / traits)");
}
return $list;
}
public function getClassAttributeListFromClassname(array|string $className, bool $deepSearch, bool $throwOnError = true) : array
{
$list = [];
foreach((array) $className as $class) {
foreach ($this->getAttributes($deepSearch) as $item) {
if ($item->object instanceof $class) {
$list[] = $item->object;
}
}
}
if ( empty($list) && $throwOnError ) {
$className = implode(',', (array) $className);
throw new \InvalidArgumentException("Attribute(s) `$className` was not found within class {$this->name} (or it's parents / traits)");
}
return $list;
}
public function getPropertyAttributeListFromClassname(array|string $className, bool $deepSearch, bool $throwOnError = true) : array
{
$list = [];
foreach((array) $className as $class) {
foreach ($this->getProperties($deepSearch) as $property) {
foreach ($property->attributes as $item) {
if ($item->object instanceof $class) {
$list[$property->name] ??= [];
$list[$property->name][] = $item->object;
}
}
}
}
if ( empty($list) && $throwOnError ) {
$className = implode(',', (array) $className);
throw new \InvalidArgumentException("Attribute(s) `$className` was not found within properties of {$this->name} (or it's parents / traits)");
}
return $list;
}
public function getMethodAttributeListFromClassname(array|string $className, bool $deepSearch, bool $throwOnError = true) : array
{
$list = [];
foreach((array) $className as $class) {
foreach ($this->getMethods($deepSearch) as $method) {
foreach ($method->attributes as $item) {
if ($item->object instanceof $class) {
$list[$method->name] ??= [];
$list[$method->name][] = $item->object;
}
}
}
}
if ( empty($list) && $throwOnError ) {
$className = implode(',', (array) $className);
throw new \InvalidArgumentException("Attribute(s) `$className` was not found within methods of {$this->name} (or it's parents / traits)");
}
return $list;
}
public function getAttribute(string $attributeType): ?object
{
foreach($this->getAttributes(true, $attributeType) as $attribute) {
if ($attribute->object instanceof $attributeType) {
return $attribute;
}
}
return null;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Notes\Common;
interface ReflectedInterface
{
public function getAttributes() : array;
}

View File

@ -0,0 +1,18 @@
<?php
namespace Notes\Common;
class ReflectedMethod extends Reflected implements ReflectedInterface
{
public function __construct(
public string $name,
public string $classname,
public false|ReflectedMethodType|\ReflectionNamedType|\ReflectionUnionType|array $type = false,
public bool $isConstructor = false,
public bool $isDestructor = false,
public bool $isAbstract = false,
public array $attributes = [],
public array $parameters = [],
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Notes\Common;
class ReflectedMethodProperty extends ReflectedProperty
{
public int $position;
public bool $optional = false;
public bool $byReference = false;
}

View File

@ -0,0 +1,8 @@
<?php
namespace Notes\Common;
class ReflectedMethodType extends ReflectedPropertyType
{
}

View File

@ -0,0 +1,41 @@
<?php
namespace Notes\Common;
use Notes\Attribute\Ignore;
class ReflectedProperty extends Reflected implements ReflectedInterface, \ArrayAccess
{
public mixed $value;
public function __construct(
public string $name,
public false|ReflectedPropertyType|array $type = false,
public array $attributes = [],
) {}
## BACKWARD COMPATIBILITY ONLY To be removed really soon !
public function offsetExists(mixed $offset): bool
{
return isset($this->$offset);
}
public function offsetGet(mixed $offset): mixed
{
if ($offset === 'tags') {
return $this->attributes;
}
return $this->$offset;
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->$offset = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->$offset);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Notes\Common;
class ReflectedPropertyType
{
public function __construct(
public string $type,
public bool $builtIn,
public bool $nullable,
) {}
public function isType(string $type) : bool
{
if ($type === "null" || $this->nullable) {
return true;
}
return $this->type === $type;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Notes\Cronard\Attribute\Method;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)]
class Cronard implements \Notes\Attribute {
public function __construct(
public string $cron,
public null|string $method = null,
) {}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Notes\Cronard\Attribute\Object;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)]
class Cronard implements \Notes\Attribute {
public function __construct(
public string $cron,
public string $method = "__invoke",
) {}
}

View File

@ -0,0 +1,99 @@
<?php namespace Notes\Cronard;
use Kash\HandleCacheTrait;
use Notes\ObjectResolver;
use Psr\SimpleCache\CacheInterface;
use RuntimeException, DirectoryIterator, Generator, Closure;
class TaskFetcher {
use HandleCacheTrait;
protected array $folderList;
protected Closure $callback;
public array $defaultMethods = [ 'GET', 'POST' ];
protected array $annotations;
public function __construct( ?array $folderList = null, ?array $annotations = null, ? CacheInterface $cache = null)
{
$this->cache = $cache;
if ($folderList !== null) {
$this->folderList = $folderList;
}
if ($annotations !== null) {
$this->annotations = $annotations;
}
else {
$this->annotations = [
'method' => [ Attribute\Method\Cronard::class ],
];
}
}
public function addFolder($folder) : void
{
$this->folderList[] = $folder;
}
public function setFolderList(array $list) : void
{
$this->folderList = $list;
}
public function scan(? array $folders = null) : Generator
{
foreach($folders ?: $this->folderList as $namespace => $folder) {
if ( ! file_exists($folder) ) {
throw new RuntimeException(sprintf("Folder `%s` can not be found or scanned", $folder));
}
foreach (new DirectoryIterator($folder) as $fileinfo) {
if ( ! $fileinfo->isDot() ) {
if ( $fileinfo->isDir() ) {
foreach($this->scan([ "{$namespace}\\" . $fileinfo->getBasename() => $fileinfo->getPathname() ]) as $ns2 => $fi2) {
yield $ns2 => $fi2;
}
}
else {
yield $namespace => $fileinfo;
}
}
}
}
}
public function compile() : array
{
return $this->handleCaching(substr(md5(serialize($this->annotations)), 0, 7), function() : array {
foreach($this->scan() as $namespace => $file) {
if ( $file->getExtension() !== "php" ) {
continue;
}
$class = $this->generateClassname($file->getBasename(".php"), $namespace);
# Should generate an equivalent of Ulmus's object reflection here !
$objectResolver = new ObjectResolver($class, $this->cache);
foreach($objectResolver->reflectedClass->getMethodAttributeListFromClassname( $this->annotations['method'], false, false ) as $func => $cronard) {
foreach($cronard as $task) {
$list[] = [ 'class' => $class, 'method' => $func, 'annotation' => $task ];
}
}
}
return $list ?? [];
});
}
protected function generateClassname($file, $namespace)
{
return "\\$namespace\\$file";
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Notes\Event\Attribute\Method;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)]
class Event implements \Notes\Attribute {
public function __construct(
public string $name,
) {}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Notes\Event\Attribute\Object;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)]
class Event implements \Notes\Attribute {
public function __construct(
public string $name,
) {}
}

View File

@ -0,0 +1,99 @@
<?php namespace Notes\Event;
use Kash\HandleCacheTrait;
use Notes\ObjectResolver;
use Psr\SimpleCache\CacheInterface;
use RuntimeException, DirectoryIterator, Generator, Closure;
class EventFetcher {
use HandleCacheTrait;
protected array $folderList;
protected Closure $callback;
public array $defaultMethods = [ 'GET', 'POST' ];
protected array $annotations;
public function __construct( ?array $folderList = null, ?array $annotations = null, ? CacheInterface $cache = null)
{
$this->cache = $cache;
if ($folderList !== null) {
$this->folderList = $folderList;
}
if ($annotations !== null) {
$this->annotations = $annotations;
}
else {
$this->annotations = [
'method' => [ Attribute\Method\Event::class ],
];
}
}
public function addFolder($folder) : void
{
$this->folderList[] = $folder;
}
public function setFolderList(array $list) : void
{
$this->folderList = $list;
}
public function scan(? array $folders = null) : Generator
{
foreach($folders ?: $this->folderList as $namespace => $folder) {
if ( ! file_exists($folder) ) {
throw new RuntimeException(sprintf("Folder `%s` can not be found or scanned", $folder));
}
foreach (new DirectoryIterator($folder) as $fileinfo) {
if ( ! $fileinfo->isDot() ) {
if ( $fileinfo->isDir() ) {
foreach($this->scan([ "{$namespace}\\" . $fileinfo->getBasename() => $fileinfo->getPathname() ]) as $ns2 => $fi2) {
yield $ns2 => $fi2;
}
}
else {
yield $namespace => $fileinfo;
}
}
}
}
}
public function compile() : array
{
return $this->handleCaching(substr(md5(serialize($this->annotations)), 0, 7), function() : array {
foreach($this->scan() as $namespace => $file) {
if ( $file->getExtension() !== "php" ) {
continue;
}
$class = $this->generateClassname($file->getBasename(".php"), $namespace);
# Should generate an equivalent of Ulmus's object reflection here !
$objectResolver = new ObjectResolver($class, $this->cache ?? null);
foreach($objectResolver->reflectedClass->getMethodAttributeListFromClassname($this->annotations['method'], false, false ) as $func => $events) {
foreach($events as $ev) {
$list[] = new \Mcnd\Event\Event(name: $ev->name, class: $class, method: $func, attribute: $ev);
}
}
}
return $list ?? [];
});
}
protected function generateClassname($file, $namespace)
{
return "\\$namespace\\$file";
}
}

View File

@ -3,6 +3,12 @@
namespace Notes; namespace Notes;
use Kash\HandleCacheTrait; use Kash\HandleCacheTrait;
use Notes\Attribute\Ignore;
use Notes\Common\ReflectedClass;
use Notes\Common\ReflectedMethod;
use Notes\Common\ReflectedMethodProperty;
use Notes\Common\ReflectedProperty;
use Notes\Common\ReflectedPropertyType;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Reflector, ReflectionClass, ReflectionProperty, ReflectionMethod, ReflectionUnionType, ReflectionNamedType, ReflectionParameter; use Reflector, ReflectionClass, ReflectionProperty, ReflectionMethod, ReflectionUnionType, ReflectionNamedType, ReflectionParameter;
@ -13,81 +19,39 @@ class ObjectReflection {
protected ReflectionClass $classReflection; protected ReflectionClass $classReflection;
public AnnotationReader $annotationReader; public function __construct(ReflectionClass|string $class, ? CacheInterface $cache = null) {
$this->classname = ltrim($class instanceof ReflectionClass ? $class->getName() : $class, '\\');
public function __construct($class, AnnotationReader $annotationReader = null, ? CacheInterface $cache = null) {
$this->classname = ltrim($class, '\\');
$this->cache = $cache; $this->cache = $cache;
#if ( ! $this->cache || ! $this->cache->has($class) ) {
$this->classReflection = $class instanceof ReflectionClass ? $class : new ReflectionClass($class); $this->classReflection = $class instanceof ReflectionClass ? $class : new ReflectionClass($class);
$this->annotationReader = $annotationReader ?: AnnotationReader::fromClass($class);
# }
} }
public static function fromClass($class, ? CacheInterface $cache = null) : self public static function fromClass(ReflectionClass|string $class, ? CacheInterface $cache = null) : self
{ {
return new static($class, null, $cache); return new static($class, $cache);
} }
public function read(bool $fullUses = true, bool $fullObject = true, $fullMethod = true, $fullProperty = true) : array public function getClassReflection() : ReflectionClass
{ {
return $this->handleCaching(implode('', [ $this->classname, (int)$fullObject, (int) $fullMethod, (int) $fullProperty ]), fn() => [ return $this->classReflection;
'uses' => $this->gatherUses($fullUses),
'class' => $this->gatherClass($fullObject),
'method' => $this->gatherMethods($fullMethod),
'property' => $this->gatherProperties($fullProperty),
]);
} }
public function gatherUses(bool $full = true) : array public function reflectClass() : ReflectedClass
{ {
if ( $full ) { $parentClass = $this->classReflection->getParentClass();
if ( $parentClass = $this->classReflection->getParentClass() ) {
$list = static::fromClass($parentClass)->gatherUses(true); return new ReflectedClass(
name: $this->classReflection->getName(),
parent: $parentClass ? static::fromClass($parentClass)->reflectClass() : false,
methods: $this->reflectMethods(),
properties: $this->reflectProperties(),
attributes: AttributeReader::reflectAttributes($this->classReflection),
interfaces: array_map(fn($interface) => static::fromClass($interface)->reflectClass(), array_keys($this->classReflection->getInterfaces())),
traits: array_map(fn($trait) => static::fromClass($trait)->reflectClass(), array_keys($this->classReflection->getTraits())),
);
} }
foreach($this->classReflection->getTraits() as $trait) { public function reflectProperties(int $filter =
$list = array_replace(static::fromClass($trait->name)->gatherUses(true), $list ?? []);
}
}
return array_replace($list ?? [], $this->getUsesStatements());
}
public function gatherClass(bool $full = true) : array
{
if ( $full ) {
if ( $parentClass = $this->classReflection->getParentClass() ) {
$class = static::fromClass($parentClass)->gatherClass(true);
}
if ( $traits = $this->classReflection->getTraits() ) {
foreach($traits as $key => $value) {
$traitTags = static::fromClass($key)->gatherClass(true);
}
}
if ( $interfaces = $this->classReflection->getInterfaces() ) {
foreach($interfaces as $key => $value) {
$interfaceTags = static::fromClass($key)->gatherClass(true);
}
}
$itemName = function($item) {
return $item->getName();
};
}
return array_merge_recursive($class ?? [], $traitTags ?? [], $interfaceTags ?? [], [
'tags' => $this->annotationReader->getClass($this->classReflection)
] + ( ! $full ? [] : [
'traits' => array_map($itemName, $traits),
'interfaces' => array_map($itemName, $interfaces),
] ));
}
public function gatherProperties(bool $full = true, int $filter =
ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PUBLIC |
ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PROTECTED |
ReflectionProperty::IS_PRIVATE ReflectionProperty::IS_PRIVATE
@ -95,207 +59,78 @@ class ObjectReflection {
{ {
$defaultValues = $this->classReflection->getDefaultProperties(); $defaultValues = $this->classReflection->getDefaultProperties();
if ( $full ) {
if ( $parentClass = $this->classReflection->getParentClass() ) {
$properties = static::fromClass($parentClass)->gatherProperties($full, $filter);
}
}
$list = []; $list = [];
foreach($this->classReflection->getProperties($filter) as $property) { foreach($this->classReflection->getProperties($filter) as $property) {
$current = [ $reflected = new ReflectedProperty($property->getName());
'name' => $property->getName() $reflected->attributes = AttributeReader::reflectAttributes($property);
];
# Default value can be 'null', so isset() it not suitable here if ( $reflected->hasIgnoreAttribute() ) {
if ( array_key_exists($current['name'], $defaultValues) ) {
$current['value'] = $defaultValues[ $current['name'] ];
}
if ( $property->hasType() ) {
$current['type'] = $property->getType()->getName();
$current['builtin'] = $property->getType()->isBuiltIn();
$current['nullable'] = $property->getType()->allowsNull();
}
$current['tags'] = $this->annotationReader->getProperty($property);
if ( $this->ignoreElementAnnotation($current['tags']) ) {
continue; continue;
} }
$list[ $current['name'] ] = $current; # Default value can be 'null', so isset() it not suitable here
if ( array_key_exists($reflected->name, $defaultValues) ) {
$reflected->value = $defaultValues[ $reflected->name ];
}
$reflected->typeFromReflection($property);
$list[ $reflected->name ] = $reflected;
} }
return array_merge($properties ?? [], $list); return array_merge($properties ?? [], $list);
} }
public function gatherMethods(bool $full = true, int $filter = public function reflectMethods(int $filter =
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED |
ReflectionMethod::IS_PROTECTED | ReflectionMethod::IS_PRIVATE | ReflectionMethod::IS_STATIC |
ReflectionMethod::IS_PRIVATE | ReflectionMethod::IS_FINAL
ReflectionMethod::IS_STATIC
) : array ) : array
{ {
$list = []; $list = [];
if ( $full ) {
if ( $parentClass = $this->classReflection->getParentClass() ) {
$methods = static::fromClass($parentClass)->gatherMethods($full, $filter);
}
}
foreach($this->classReflection->getMethods($filter) as $method) { foreach($this->classReflection->getMethods($filter) as $method) {
if ( ! $full && ( $method->class !== $this->classname ) ) { # Skipping parent's methods, we'll retrieve them in its own reflection
if ( $method->class !== $this->classname ) {
continue; continue;
} }
$parameters = []; $reflectedMethod = new ReflectedMethod(
name: $method->getName(),
classname: $method->class,
type: $method->hasReturnType() ? $method->getReturnType() : false,
isConstructor: $method->isConstructor(),
isDestructor: $method->isDestructor(),
isAbstract: $method->isAbstract(),
attributes: AttributeReader::reflectAttributes($method),
);
if ( $reflectedMethod->hasIgnoreAttribute() ) {
continue;
}
foreach($method->getParameters() as $parameter) { foreach($method->getParameters() as $parameter) {
$parameters[$parameter->getName()] = [ $reflectedParameter = new ReflectedMethodProperty(
'null' => $parameter->allowsNull(), name: $parameter->getName(),
'position' => $parameter->getPosition(), attributes: AttributeReader::reflectAttributes($parameter),
'type' => $parameter->hasType() && $parameter->getType() instanceof \ReflectionNamedType ? $parameter->getType()->getName() : false, );
'array' => $this->isType('array', $parameter),
'callable' => $this->isType('callable', $parameter),
'optional' => $parameter->isOptional(),
'byReference' => $parameter->isPassedByReference(),
];
}
$current = [ if ( $reflectedParameter->hasIgnoreAttribute() ) {
'name' => $method->getName(),
'type' => $method->hasReturnType() ? $method->getReturnType()->getName() : false,
'constructor' => $method->isConstructor(),
'destructor' => $method->isDestructor(),
'parameters' => $parameters,
];
$current['tags'] = $this->annotationReader->getMethod($method);
if ( $this->ignoreElementAnnotation($current['tags']) ) {
continue; continue;
} }
$list[ $current['name'] ] = $current; $reflectedParameter->position = $parameter->getPosition();
$reflectedParameter->typeFromReflection($parameter);
$reflectedParameter->byReference = $parameter->isPassedByReference();
$reflectedParameter->optional = $parameter->isOptional();
$reflectedMethod->parameters[$reflectedParameter->name] = $reflectedParameter;
} }
return array_merge($methods ?? [], $list); $list[$reflectedMethod->name] = $reflectedMethod;
} }
protected function ignoreElementAnnotation($tags) : bool return $list;
{
return in_array('IGNORE', array_map('strtoupper', array_column($tags, 'tag') ));
}
protected function readCode() : string
{
static $code = [];
$fileName = $this->classReflection->getFilename();
return $code[$fileName] ?? $code[$fileName] = file_get_contents($fileName);
}
# From https://www.php.net/manual/en/reflectionparameter.isarray.php
public static function isType(string $type, ReflectionParameter $reflectionParameter) : bool
{
if ( $reflectionType = $reflectionParameter->getType() ) {
$types = $reflectionType instanceof ReflectionUnionType ? $reflectionType->getTypes() : [$reflectionType];
return in_array($type, array_map(fn(ReflectionNamedType $t) => $t->getName(), $types));
}
return false;
}
protected function getUsesStatements() : array
{
$uses = [];
$tokens = token_get_all( $c = $this->readCode() );
while ( $token = array_shift($tokens) ) {
if ( is_array($token) ) {
list($token, $value) = $token;
}
switch ($token) {
case T_CLASS:
case T_TRAIT:
case T_INTERFACE:
break 2;
case T_USE:
$isUse = true;
break;
case T_NS_SEPARATOR:
$isNamespace = $isUse;
break;
case T_NAME_QUALIFIED:
case T_STRING:
if ( $isNamespace && $latestString ) {
$statement[] = $latestString;
}
$latestString = $value;
break;
case T_AS:
# My\Name\Space\aClassHere `as` ClassAlias;
$replacedClass = implode("\\", array_merge($statement, [ $latestString ]));
$latestString = null;
break;
case T_WHITESPACE:
case T_COMMENT:
case T_DOC_COMMENT:
break;
case '{':
# opening a sub-namespace -> \My\Name\Space\`{`OneItem, AnotherItem}
if ( $isNamespace ) {
$inNamespace = true;
}
break;
case ';';
case ',':
case '}':
if ( $isUse ) {
$clsName = ltrim(substr($latestString, strrpos($latestString, "\\") ), '\\');
if ( $replacedClass ) {
$uses[$replacedClass] = $clsName;
$replacedClass = "";
}
elseif ( $latestString ) {
$uses[implode("\\", array_merge($statement, [ $latestString ]))] = $clsName;
}
}
if ( $inNamespace ) {
$latestString = "";
# \My\Name\Space\{OneItem, AnotherItem`}` <- closing a sub-namespace
if ( $token !== "}" ) {
break;
}
}
case T_OPEN_TAG:
default:
$statement = [];
$latestString = "";
$replacedClass = null;
$isNamespace = $inNamespace = false;
$isUse = ( $isUse ?? false ) && ( $token === ',' );
break;
}
}
return $uses;
} }
} }

View File

@ -2,16 +2,13 @@
namespace Notes; namespace Notes;
use Notes\Common\ReflectedClass;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
class ObjectResolver { class ObjectResolver {
const KEY_ENTITY_NAME = 01;
const KEY_COLUMN_NAME = 02;
public string $objectClass; public string $objectClass;
public array $uses; public ReflectedClass $reflectedClass;
public array $class; public array $class;
@ -19,52 +16,11 @@ class ObjectResolver {
public array $methods; public array $methods;
public function __construct(string $objectClass, bool $fullUses = true, bool $fullObject = true, $fullMethod = true, $fullProperty = true, ? CacheInterface $cache = null) public function __construct(string $objectClass, ? CacheInterface $cache = null)
{ {
$this->objectClass = $objectClass; $this->objectClass = $objectClass;
list($this->uses, $this->class, $this->methods, $this->properties) = array_values( $this->reflectedClass = ObjectReflection::fromClass($objectClass, $cache)->reflectClass();
ObjectReflection::fromClass($objectClass, $cache)->read($fullUses, $fullObject, $fullMethod, $fullProperty)
);
$this->resolveAnnotations();
}
/**
* Transform an annotation into it's object's counterpart
*/
public function getAttributeFromClassname(array|string $className, bool $throwOnError = true) : object|bool
{
foreach((array) $className as $class) {
foreach (array_reverse($this->class['tags']) as $item) {
if ($item['object'] instanceof $class) {
return $this->instanciateAnnotationObject($item);
}
foreach ($this->properties as $property) {
foreach ($property['tags'] as $tag) {
if ($item['object'] instanceof $class) {
return $this->instanciateAnnotationObject($tag);
}
}
}
foreach ($this->methods as $method) {
foreach ($method['tags'] as $tag) {
if ($item['object'] instanceof $class) {
return $this->instanciateAnnotationObject($tag);
}
}
}
}
}
if ($throwOnError) {
throw new \Exception(sprintf("Annotation `%s` could not be found within your object `%s`.", implode(', ', (array)$className), $this->objectClass));
}
return false;
} }
/** /**
@ -75,27 +31,26 @@ class ObjectResolver {
$list = []; $list = [];
foreach((array) $className as $class) { foreach((array) $className as $class) {
foreach ($this->class['tags'] as $item) { foreach ($this->reflectedClass->getAttributes() as $item) {
if ($item['object'] instanceof $class) { if ($item->object instanceof $class) {
$list[] = $this->instanciateAnnotationObject($item); $list[] = $item->object;
} }
} }
foreach ($this->properties as $property) { foreach ($this->reflectedClass->getProperties(true) as $property) {
foreach ($property['tags'] as $item) { foreach ($property->attributes as $item) {
if ($item['object'] instanceof $class) { if ($item->object instanceof $class) {
$list[$property['name']] ??= []; $list[$property->name] ??= [];
$list[$property->name][] = $item->object;
$list[$property['name']][] = $this->instanciateAnnotationObject($item);
} }
} }
} }
foreach ($this->methods as $method) { foreach ($this->reflectedClass->getMethods(true) as $method) {
foreach ($method['tags'] as $item) { foreach ($method->attributes as $item) {
if ($item['object'] instanceof $class) { if ($item->object instanceof $class) {
$list[$method['name']] ??= []; $list[$method->name] ??= [];
$list[$method['name']][] = $this->instanciateAnnotationObject($item); $list[$method->name][] = $item->object;
} }
} }
} }
@ -107,148 +62,4 @@ class ObjectResolver {
return $list; return $list;
} }
/**
* @deprecated Will soon be deprecated in favour of PHP's native attributes
* Transform an annotation into it's object's counterpart
*/
public function getAnnotationFromClassname(array|string $className, bool $throwOnError = true) : object|bool
{
foreach((array) $className as $class) {
if ( $name = $this->uses[$class] ?? false) {
foreach (array_reverse($this->class['tags']) as $item) {
if ($item['tag'] === $name) {
return $this->instanciateAnnotationObject($item);
}
foreach ($this->properties as $property) {
foreach ($property['tags'] as $item) {
if ($item['tag'] === $name) {
return $this->instanciateAnnotationObject($item);
}
}
}
foreach ($this->methods as $method) {
foreach ($method['tags'] as $item) {
if ($item['tag'] === $name) {
return $this->instanciateAnnotationObject($item);
}
}
}
}
}
}
if ($throwOnError) {
throw new \Exception(sprintf("Annotation `%s` could not be found within your object `%s`.", implode(', ', (array)$className), $this->objectClass));
}
return false;
}
/**
* Transform an annotation into it's object's counterpart
*/
public function getAnnotationListFromClassname(array|string $className, bool $throwOnError = true) : array
{
$list = [];
foreach((array) $className as $class) {
if ($name = $this->uses[$class] ?? false) {
foreach ($this->class['tags'] as $item) {
if ($item['tag'] === $name) {
$list[] = $this->instanciateAnnotationObject($item);
}
}
foreach ($this->properties as $property) {
foreach ($property['tags'] as $item) {
if ($item['tag'] === $name) {
$list[$property['name']] ??= [];
$list[$property['name']][] = $this->instanciateAnnotationObject($item);
}
}
}
foreach ($this->methods as $method) {
foreach ($method['tags'] as $item) {
if ($item['tag'] === $name) {
$list[$method['name']] ??= [];
$list[$method['name']][] = $this->instanciateAnnotationObject($item);
}
}
}
}
}
if ( empty($list) ) {
if ($throwOnError) throw new \InvalidArgumentException("Class `$className` was not found within {$this->objectClass} uses statement (or it's children / traits)");
}
return $list;
}
public function instanciateAnnotationObject(array $tagDefinition) : Annotation
{
if (isset($tagDefinition['object']) && $tagDefinition['object'] instanceof \Attribute) {
return $tagDefinition['object'];
}
$arguments = $this->extractArguments($tagDefinition['arguments']);
if ( false === $class = array_search($tagDefinition['tag'], $this->uses) ) {
return new class() implements Annotation {};
# throw new \InvalidArgumentException("Annotation class `{$tagDefinition['tag']}` was not found within {$this->objectClass} uses statement (or it's children / traits)");
}
$obj = new $class(... $arguments['constructor']);
foreach($arguments['setter'] as $key => $value) {
$obj->$key = $value;
}
return $obj;
}
/**
* Extracts arguments from an Annotation definition, easing object's declaration.
*/
protected function extractArguments(array $arguments) : array
{
$list = [
'setter' => [],
'constructor' => [],
];
ksort($arguments);
foreach($arguments as $key => $value) {
$list[ is_int($key) ? 'constructor' : 'setter' ][$key] = $value;
}
return $list;
}
protected function resolveAnnotations()
{
foreach($this->class['tags'] as $key => &$tag) {
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
}
foreach($this->properties as &$property) {
foreach($property['tags'] as &$tag){
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
}
}
foreach($this->methods as &$method) {
foreach($method['tags'] as &$tag){
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
}
}
}
} }

View File

@ -0,0 +1,57 @@
<?php
namespace Notes\Route\Attribute\Method;
use Psr\Http\Message\ServerRequestInterface;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)]
class Route implements \Notes\Attribute {
public function __construct(
public string $route,
public string $name,
public string|array $method = [ "GET", "POST" ],
public ? string $base = "",
public ? string $class = null,
public ? string $classMethod = null,
public bool $currentRoute = false,
public string $description = "",
) {}
public function getRoute() : string
{
return rtrim("/" . trim(isset($this->base) ?
"/" . trim($this->base, "/") . "/" . ltrim($this->route, "/")
:
"/" . ltrim($this->route, "/"), "/"), '/');
}
public function getRegistrableRoute() : string
{
return preg_replace('/(\=.*)(?=\})/i', '', $this->getRoute());
}
public function matchRouteName(string $name) : bool
{
return strtolower($this->name) === strtolower($name);
}
public function match(mixed $name) : bool
{
foreach((array) $name as $item) {
if ( fnmatch($item, $this->name) ) {
return true;
}
}
return false;
}
public function isRoute(string $name) {
return $this->name === $name;
}
public function currentRoute(bool|null $value = null ) {
return is_null($value) ? $value : $this->currentRoute = $value;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Notes\Route\Attribute\Object;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Route implements \Notes\Attribute {
public function __construct(
public string|array $method = [ "GET", "POST" ],
# NULL will fallback on nearest base
public null|string $base = null,
){}
}

147
src/Route/RouteFetcher.php Normal file
View File

@ -0,0 +1,147 @@
<?php namespace Notes\Route;
use Kash\HandleCacheTrait;
use Notes\ObjectReflection,
Notes\ObjectResolver;
use Psr\SimpleCache\CacheInterface;
use RuntimeException, DirectoryIterator, Generator, Closure;
class RouteFetcher {
use HandleCacheTrait;
public array $list = [];
protected bool $debug;
protected array $folderList;
protected Closure $callback;
public array $defaultMethods = [ 'GET', 'POST' ];
protected array $attributes;
public function __construct(?Closure $callback = null, ? array $folderList = null, ? array $attributes = null, ? CacheInterface $cache = null)
{
$this->cache = $cache;
if ($callback !== null) {
$this->callback = $callback;
}
if ($folderList !== null) {
$this->folderList = $folderList;
}
if ($attributes !== null) {
$this->attributes = $attributes;
}
else {
$this->attributes = [
'object' => [ Attribute\Object\Route::class ],
'method' => [ Attribute\Method\Route::class ],
];
}
}
public function addFolder($folder) : void
{
$this->folderList[] = $folder;
}
public function setFolderList(array $list) : void
{
$this->folderList = $list;
}
public function scan(? array $folders = null) : Generator
{
foreach($folders ?: $this->folderList as $namespace => $folder) {
if ( ! file_exists($folder) ) {
throw new RuntimeException(sprintf("Folder `%s` can not be found or scanned", $folder));
}
foreach (new DirectoryIterator($folder) as $fileinfo) {
if ( ! $fileinfo->isDot() ) {
if ( $fileinfo->isDir() ) {
foreach($this->scan([ "{$namespace}\\" . $fileinfo->getBasename() => $fileinfo->getPathname() ]) as $ns2 => $fi2) {
yield $ns2 => $fi2;
}
}
else {
yield $namespace => $fileinfo;
}
}
}
}
}
public function compile(null|array $attributes = null) : array
{
$attributes ??= $this->attributes;
return $this->handleCaching(substr(md5(serialize($attributes)), 0, 7), function() use ($attributes) : array {
$list = [];
foreach($this->scan() as $namespace => $file) {
if ( $file->getExtension() !== "php" ) {
continue;
}
$base = null;
$class = $this->generateClassname($file->getBasename(".php"), $namespace);
$methods = null;
$objectResolver = new ObjectResolver($class, $this->cache);
if ( isset($attributes['object']) ) {
$objects = $objectResolver->reflectedClass->getClassAttributeListFromClassname($attributes['object'], true, false);
foreach($objects as $object) {
if ($object->method ?? false ) {
$methods ??= (array) $object->method;
}
$base ??= $object->base ?? null;
}
}
if ( isset($attributes['method']) ) {
$routeList = $objectResolver->reflectedClass->getMethodAttributeListFromClassname( $attributes['method'], false, false );
foreach($routeList as $func => $routes) {
if (is_array($routes)) {
foreach ($routes as $route) {
if ($route instanceof \Notes\Route\Attribute\Method\Route) {
$route->base = $route->base ?: $base;
}
$route->class = $class;
$route->classMethod = $func;
if (false === ($route->methods ?? false)) {
$route->methods = $methods ?? $this->defaultMethods;
}
if (false !== ($this->callback ?? false)) {
call_user_func_array($this->callback, [$route]);
}
$list[] = $route;
}
}
}
}
}
return $list;
});
}
protected function generateClassname($file, $namespace)
{
return "\\$namespace\\$file";
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Notes\Security\Attribute;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
class Security implements \Notes\Attribute {
public function __construct(
public null|bool $locked = null,
public null|string $realm = null,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Notes\Security\Attribute;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
class Taxus implements \Notes\Attribute {
public function __construct(
public null|string|\BackedEnum $privilege = null,
public null|string $module = null,
public string|array $method = "*",
) {}
}

View File

@ -0,0 +1,117 @@
<?php declare(strict_types = 1);
namespace Notes\Security;
use Notes\Common\ReflectedAttribute;
use Taxus\Taxus;
use Psr\Http\Message\ResponseInterface;
use Notes\ObjectResolver;
use Taxus\TaxusGrantEnum;
class SecurityHandler {
protected ResponseInterface $redirectResponse;
protected \Closure|ResponseInterface|null $unauthorizeResponse;
protected ? Taxus $taxus;
public function __construct(ResponseInterface $redirectResponse, \Closure|ResponseInterface|null $unauthorizeResponse = null, ? Taxus $taxus = null) {
$this->redirectResponse = $redirectResponse;
$this->unauthorizeResponse = $unauthorizeResponse;
$this->taxus = $taxus;
}
public function verify(string $className, string $methodName) : ? ResponseInterface
{
return $this->isLocked($className, $methodName) ? $this->redirectResponse : null;
}
# @TODO Must HANDLE REALM !
public function isLocked(string $className, string $methodName) : bool
{
# Searching method first and fallbacking on object if none found
if ( $list = $this->findAttributes(Attribute\Security::class, $className, $methodName) ) {
return array_shift($list)->locked;
}
return true;
}
public function taxus(string $className, string $methodName, ... $arguments) : ? ResponseInterface
{
$granted = $this->hasGrantPermission($className , $methodName, ...$arguments);
if ($granted) {
return null;
}
else {
if ($this->unauthorizeResponse) {
if ($this->unauthorizeResponse instanceof ResponseInterface) {
return $this->unauthorizeResponse;
}
return call_user_func_array($this->unauthorizeResponse, [ ...$arguments, ['method' => $methodName, 'object' => $className ], $className, $methodName ]);
}
else {
throw new \ErrorException("Unauthorized response given.");
}
}
}
public function hasGrantPermission(string $className, string $methodName, ... $arguments) : bool
{
$fromObject = $this->findAttributes(Attribute\Taxus::class, $className);
$fromMethod = $this->findAttributes(Attribute\Taxus::class, $className, $methodName);
if ($fromMethod || $fromObject) {
if ( $this->taxusGrantPermission($fromMethod, ... $arguments) || $this->taxusGrantPermission($fromObject, ... $arguments) ) {
return true;
}
return false;
}
return true;
}
protected function findAttributes(string $attribute, string $class, ? string $method = null) : array
{
$objectResolver = new ObjectResolver($class);
if ($method) {
$fromMethod = $objectResolver->reflectedClass->getMethods( false );
if (isset($fromMethod[$method])) {
$attributeList = $fromMethod[$method]->getAttributes($attribute);
if ($attributeList) {
return array_map(fn(ReflectedAttribute $ref) => $ref->object, $attributeList);
}
}
}
$attributeList = $objectResolver->reflectedClass->getAttributes(false, $attribute) ?: $objectResolver->reflectedClass->getAttributes(true, $attribute);
if ($attributeList) {
return array_map(fn(ReflectedAttribute $ref) => $ref->object, $attributeList);
}
return [];
}
protected function taxusGrantPermission(array $attributeList, ...$arguments) : bool
{
foreach ($attributeList as $item) {
if ( $grant = $this->taxus->granted($item->privilege, ... array_merge($arguments, [ $item ])) ) {
if (is_bool($grant) ? $grant : $grant === TaxusGrantEnum::Authorized) {
return true;
}
}
}
return false;
}
}