17 Commits
Author SHA1 Message Date
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 1599284334 - Added route description 2024-05-21 13:03:18 +00:00
mcndave 2345c5453f - Added MatchRoute Method 2024-05-09 19:52:10 +00:00
Dave Mc Nicoll 996c8fa14a - Fixed Notes bugs with Union Types Reflection 2024-04-24 11:24:27 -04:00
mcndave 340235e2f8 - Base can now be null 2023-11-20 13:40:21 -05:00
mcndave 124bddb238 - Latest work on route base fallbacks 2023-11-17 22:38:26 -05:00
mcndave 0fcff83eac - Fixed missing tags from traits 2023-11-17 21:59:29 -05:00
mcndave 5f00c2b603 - Fixed a typo 2023-11-17 19:57:47 +00:00
mcndave cee97fca25 - Merged multiple sub-libraries 2023-11-17 19:47:24 +00:00
mcndave 767152e0c3 - Removing annotations replaced by attributes 2023-11-03 19:54:44 -04:00
mcndave 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
mcndave aca093dd97 - Fixed a bug with Union types within methods arguments 2023-02-07 16:50:16 +00:00
23 changed files with 886 additions and 37 deletions

No files matched your search

+27 -18
View File
@@ -3,7 +3,7 @@
array_shift($argv);
$opt = getopt('c::f:v', [ 'folder:', 'confirm', 'verbose' ]);
$opt = getopt('c::f:v', [ 'folder:', 'confirm', 'verbose', 'clean', 'old' ]);
if ( ! $opt ) {
exit("Bad arguments provided, you must specify a fullpath using the -f or --folder option\n");
@@ -30,16 +30,30 @@ foreach ($iterator as $info) {
$content = file_get_contents($info->getPathname());
if ( isset($opt['clean']) ) {
foreach (explode(PHP_EOL, $content) as $lineIdx => $line) {
if ( $pos = strpos($line, '# migrated from: ') ) {
$attr = true;
$newContent[] = $newLine = substr($line, 0, $pos);
echo sprintf("Changed line '%s' \n", trim($newLine));
}
else {
$newContent[] = $line;
}
}
}
else {
foreach (explode(PHP_EOL, $content) as $lineIdx => $line) {
$tline = trim($line);
if ($tline === "/**") {
$opened = true;
}
elseif ($tline === '*/') {
} elseif ($tline === '*/') {
$opened = false;
}
elseif (substr($tline, 0, 1) === '*') {
} elseif (substr($tline, 0, 1) === '*') {
$annotation = trim(substr($tline, 1));
if (strpos($annotation, '@') === 0) {
@@ -58,44 +72,39 @@ foreach ($iterator as $info) {
if (is_array($argument)) {
$argument = var_export($argument, true);
}
elseif ( is_bool($argument) ) {
} elseif (is_bool($argument)) {
$argument = $argument ? 'true' : 'false';
}
elseif (! is_numeric($argument) ) {
} elseif (!is_numeric($argument)) {
$argument = "\"$argument\"";
}
if (is_numeric($key)) {
$renderedArgs[] = $argument;
}
else {
} else {
$renderedArgs[] = "$key: $argument";
}
}
$renderedArgs = implode(', ', $renderedArgs);
}
catch(\Throwable $e) {
} 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 {
} 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" : "");
$attr[] = $attribute = sprintf("%s#[%s%s]%s", str_repeat(" ", strlen($line) - strlen(ltrim($line)) >= 4 ? 4 : 0), $name, $renderedArgs ? "($renderedArgs)" : "", $renderedArgs ? ( isset($opt['old']) ? " # migrated from: $args" : "" ) : "");
$newContent[] = $attribute;
}
}
else {
} else {
$newContent[] = $line;
}
}
}
$newContent = implode(PHP_EOL, $newContent);
+1 -1
View File
@@ -6,7 +6,7 @@
"authors": [
{
"name": "Dave Mc Nicoll",
"email": "mcndave@gmail.com"
"email": "info@mcnd.ca"
}
],
"require": {},
+5 -3
View File
@@ -34,12 +34,12 @@ class AnnotationReader
return $this->parseDocComment($method);
}
protected function parseDocComment(Reflector $reflect)
protected function parseDocComment(\Reflector $reflect)
{
$tags = [];
if ( $reflect->getAttributes() ) {
foreach($reflect->getAttributes() as $attr) {
if ( $attributes = $reflect->getAttributes() ) {
foreach($attributes as $attr) {
try {
$tags[] = [
'tag' => substr(strrchr($attr->getName(), "\\"), 1),
@@ -99,6 +99,8 @@ class AnnotationReader
case $reflect instanceof ReflectionClass :
return $reflect->name;
}
return "#UNKNOWN#";
}
protected function getObjectNamespace(Reflector $reflect) : string
+6
View File
@@ -0,0 +1,6 @@
<?php
namespace Notes\Attribute;
#[\Attribute(\Attribute::TARGET_ALL)]
class Ignore implements \Notes\Attribute {}
@@ -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 = "",
) {}
}
+75
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;
}
}
+17
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,
) {}
}
+10
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
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, false, true, false, false);
$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";
}
}
+11
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,
) {}
}
+11
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",
) {}
}
+99
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, true, true, false, true);
foreach($objectResolver->getAttributeListFromClassname( $this->annotations['method'], 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";
}
}
+10
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,
) {}
}
+10
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,
) {}
}
+99
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, true, true, false, true);
foreach($objectResolver->getAttributeListFromClassname( $this->annotations['method'], 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";
}
}
+25 -11
View File
@@ -3,6 +3,7 @@
namespace Notes;
use Kash\HandleCacheTrait;
use Notes\Attribute\Ignore;
use Psr\SimpleCache\CacheInterface;
use Reflector, ReflectionClass, ReflectionProperty, ReflectionMethod, ReflectionUnionType, ReflectionNamedType, ReflectionParameter;
@@ -108,21 +109,30 @@ class ObjectReflection {
'name' => $property->getName()
];
$current['tags'] = $this->annotationReader->getProperty($property);
if ( $this->ignoreElementAnnotation($current['tags']) ) {
continue;
}
# Default value can be 'null', so isset() it not suitable here
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();
$type = $property->getType();
if ($type instanceof \ReflectionUnionType ) {
foreach($type->getTypes() as $type) {
# dump($type->getName(), $type->isBuiltin(), $type->allowsNull());
}
}
else {
$current['type'] = $type->getName();
$current['builtin'] = $type->isBuiltIn();
$current['nullable'] = $type->allowsNull();
}
$current['tags'] = $this->annotationReader->getProperty($property);
if ( $this->ignoreElementAnnotation($current['tags']) ) {
continue;
}
$list[ $current['name'] ] = $current;
@@ -135,7 +145,8 @@ class ObjectReflection {
ReflectionMethod::IS_PUBLIC |
ReflectionMethod::IS_PROTECTED |
ReflectionMethod::IS_PRIVATE |
ReflectionMethod::IS_STATIC
ReflectionMethod::IS_STATIC |
ReflectionMethod::IS_FINAL
) : array
{
$list = [];
@@ -167,7 +178,8 @@ class ObjectReflection {
$current = [
'name' => $method->getName(),
'type' => $method->hasReturnType() ? $method->getReturnType()->getName() : false,
'classname' => $method->class,
'type' => $method->hasReturnType() && $method->getReturnType() instanceof \ReflectionNamedType ? $method->getReturnType()->getName() : false,
'constructor' => $method->isConstructor(),
'destructor' => $method->isDestructor(),
'parameters' => $parameters,
@@ -187,7 +199,7 @@ class ObjectReflection {
protected function ignoreElementAnnotation($tags) : bool
{
return in_array('IGNORE', array_map('strtoupper', array_column($tags, 'tag') ));
return [] !== array_filter($tags, fn($e) => ( $e['object'] ?? null ) instanceof Ignore);
}
@@ -212,6 +224,8 @@ class ObjectReflection {
protected function getUsesStatements() : array
{
return [];
$uses = [];
$tokens = token_get_all( $c = $this->readCode() );
+12 -4
View File
@@ -85,7 +85,6 @@ class ObjectResolver {
foreach ($property['tags'] as $item) {
if ($item['object'] instanceof $class) {
$list[$property['name']] ??= [];
$list[$property['name']][] = $this->instanciateAnnotationObject($item);
}
}
@@ -192,9 +191,9 @@ class ObjectResolver {
return $list;
}
public function instanciateAnnotationObject(array $tagDefinition) : Annotation
public function instanciateAnnotationObject(array $tagDefinition) : Annotation|\Notes\Attribute
{
if (isset($tagDefinition['object']) && $tagDefinition['object'] instanceof \Attribute) {
if (isset($tagDefinition['object']) && $tagDefinition['object'] instanceof \Notes\Attribute) {
return $tagDefinition['object'];
}
@@ -204,13 +203,22 @@ class ObjectResolver {
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)");
}
try {
$obj = new $class(... $arguments['constructor']);
}
catch (\Throwable $e) {
$err = $e::class;
throw new $err(sprintf("An error occured trying to instanciate class definition '%s' given argument '%s' : %s", var_export($tagDefinition, true), var_export($arguments['constructor'], true), $e->getMessage()));
}
foreach($arguments['setter'] as $key => $value) {
$obj->$key = $value;
}
if ( ! $obj instanceof \Notes\Attribute ) {
throw new \RuntimeException(sprintf("An error occurred trying to retrieve an Attribute def:%s from class %s", json_encode($tagDefinition), $this->objectClass));
}
return $obj;
}
+52
View File
@@ -0,0 +1,52 @@
<?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 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;
}
}
+12
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
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, true, true, false, true, $this->cache);
if ( isset($attributes['object']) ) {
$objects = $objectResolver->getAttributeListFromClassname($attributes['object'], false);
foreach(array_reverse($objects) as $object) {
if ($object->method ?? false ) {
$methods ??= (array) $object->method;
}
$base ??= $object->base ?? null;
}
}
if ( isset($attributes['method']) ) {
$routeList = $objectResolver->getAttributeListFromClassname( $attributes['method'], 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";
}
}
+11
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,
) {}
}
+11
View File
@@ -0,0 +1,11 @@
<?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,
) {}
}
+75
View File
@@ -0,0 +1,75 @@
<?php declare(strict_types = 1);
namespace Notes\Security;
use Taxus\Taxus;
use Psr\Http\Message\ResponseInterface;
use Notes\ObjectResolver;
class SecurityHandler {
protected ResponseInterface $redirectResponse;
protected ? \Closure $unauthorizeResponse;
protected ? Taxus $taxus;
public function __construct(ResponseInterface $redirectResponse, ? \Closure $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;
}
public function isLocked(string $className, string $methodName) : bool
{
if ( $security = $this->getClassAttributes(Attribute\Security::class, $className, $methodName) ) {
return array_pop($security)->locked;
}
return true;
}
public function taxus(string $className, string $methodName, object $user = null) : ? ResponseInterface
{
if ($taxus = $this->getClassAttributes(Attribute\Taxus::class, $className, $methodName)) {
if ($this->unauthorizeResponse) {
foreach ($taxus as $item) {
if (!isset($item->privilege) || $this->taxus->granted($item->privilege, $user, $item)) {
return null;
}
}
return call_user_func_array($this->unauthorizeResponse, [ $user, $taxus, $className, $methodName ]);
}
else {
throw new \ErrorException("Unauthorized response given.");
}
}
return null;
}
protected function getClassAttributes(string $annotationClass, string $className, string $methodName)/* : \Notes\Attribute|array */
{
$objectResolver = new ObjectResolver($className, true, true, false, true);
try {
$method = $objectResolver->getAttributeListFromClassname( $annotationClass, false );
}
catch(\Exception $e) { }
if ( $method[$methodName] ?? false ) {
return $method[$methodName];
}
else {
return array_filter($method, fn($e) => is_object($e));
}
}
}