Compare commits
22
Commits
1.0
..
fcb332ae53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcb332ae53 | ||
|
|
133672b347 | ||
|
|
1599284334 | ||
|
|
2345c5453f | ||
|
|
996c8fa14a | ||
|
|
340235e2f8 | ||
|
|
124bddb238 | ||
|
|
0fcff83eac | ||
|
|
5f00c2b603 | ||
|
|
cee97fca25 | ||
|
|
767152e0c3 | ||
|
|
dd0f0271a6 | ||
|
|
7c7d8c9d8d | ||
|
|
e3e922e3bf | ||
|
|
564a05a1d4 | ||
|
|
1accac94c0 | ||
|
|
aca093dd97 | ||
|
|
0ba4fe3a51 | ||
|
|
d92bb527da | ||
|
|
87b9ea2e2a | ||
|
|
a876ea9045 | ||
|
|
f4fffbd480 |
No files matched your search
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
|
||||
array_shift($argv);
|
||||
|
||||
$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");
|
||||
}
|
||||
|
||||
$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());
|
||||
|
||||
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 === '*/') {
|
||||
$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 ? ( isset($opt['old']) ? " # 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());
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Dave Mc Nicoll",
|
||||
"email": "mcndave@gmail.com"
|
||||
"email": "info@mcnd.ca"
|
||||
}
|
||||
],
|
||||
"require": {},
|
||||
|
||||
@@ -34,22 +34,38 @@ class AnnotationReader
|
||||
return $this->parseDocComment($method);
|
||||
}
|
||||
|
||||
protected function parseDocComment(Reflector $reflect)
|
||||
protected function parseDocComment(\Reflector $reflect)
|
||||
{
|
||||
$namespace = $this->getObjectNamespace($reflect);
|
||||
$tags = [];
|
||||
|
||||
foreach(preg_split("/\r\n|\n|\r/", $reflect->getDocComment()) as $line) {
|
||||
if ( $attributes = $reflect->getAttributes() ) {
|
||||
foreach($attributes as $attr) {
|
||||
try {
|
||||
$tags[] = [
|
||||
'tag' => substr(strrchr($attr->getName(), "\\"), 1),
|
||||
'arguments' => $attr->getArguments(),
|
||||
'object' => $attr->newInstance(),
|
||||
];
|
||||
}
|
||||
catch(\Throwable $e) {
|
||||
throw new \Exception(sprintf("%s for %s in file '%s'", $e->getMessage(), $reflect, $this->class));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$namespace = $this->getObjectNamespace($reflect);
|
||||
|
||||
foreach (preg_split("/\r\n|\n|\r/", $reflect->getDocComment()) as $line) {
|
||||
$line = ltrim($line, "* \t\/");
|
||||
$line = rtrim($line, "\t ");
|
||||
|
||||
if ( substr($line, 0, 1) === '@' ) {
|
||||
if (substr($line, 0, 1) === '@') {
|
||||
$line = ltrim($line, '@');
|
||||
|
||||
$open = strpos($line, "(");
|
||||
$close = strrpos($line, ")");
|
||||
|
||||
if ( ! in_array(false, [ $open, $close ], true) && ( ++$open !== $close ) ) {
|
||||
if (!in_array(false, [$open, $close], true) && (++$open !== $close)) {
|
||||
$arguments = substr($line, $open, $close - $open);
|
||||
|
||||
try {
|
||||
@@ -57,12 +73,10 @@ class AnnotationReader
|
||||
'tag' => substr($line, 0, $open - 1),
|
||||
'arguments' => eval("namespace $namespace; return [ $arguments ];"),
|
||||
];
|
||||
}
|
||||
catch(\Throwable $error) {
|
||||
} catch (\Throwable $error) {
|
||||
throw new \InvalidArgumentException("An error occured while parsing annotation from '" . $this->getObjectName($reflect) . "' : @$line -- " . $error->getMessage());
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$tags[] = [
|
||||
'tag' => $line,
|
||||
'arguments' => [],
|
||||
@@ -70,6 +84,7 @@ class AnnotationReader
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
@@ -84,6 +99,8 @@ class AnnotationReader
|
||||
case $reflect instanceof ReflectionClass :
|
||||
return $reflect->name;
|
||||
}
|
||||
|
||||
return "#UNKNOWN#";
|
||||
}
|
||||
|
||||
protected function getObjectNamespace(Reflector $reflect) : string
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Notes;
|
||||
|
||||
interface Attribute {}
|
||||
@@ -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 = "",
|
||||
) {}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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",
|
||||
) {}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
+40
-17
@@ -2,33 +2,43 @@
|
||||
|
||||
namespace Notes;
|
||||
|
||||
use Kash\HandleCacheTrait;
|
||||
use Notes\Attribute\Ignore;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Reflector, ReflectionClass, ReflectionProperty, ReflectionMethod, ReflectionUnionType, ReflectionNamedType, ReflectionParameter;
|
||||
|
||||
class ObjectReflection {
|
||||
use HandleCacheTrait;
|
||||
|
||||
protected string $classname;
|
||||
|
||||
protected ReflectionClass $classReflection;
|
||||
|
||||
public AnnotationReader $annotationReader;
|
||||
|
||||
public function __construct($class, AnnotationReader $annotationReader = null) {
|
||||
public function __construct($class, AnnotationReader $annotationReader = null, ? CacheInterface $cache = null) {
|
||||
$this->classname = ltrim($class, '\\');
|
||||
$this->cache = $cache;
|
||||
|
||||
# if ( ! $this->cache || ! $this->cache->has($class) ) {
|
||||
$this->classReflection = $class instanceof ReflectionClass ? $class : new ReflectionClass($class);
|
||||
$this->annotationReader = $annotationReader ?: AnnotationReader::fromClass($class);
|
||||
# }
|
||||
}
|
||||
|
||||
public static function fromClass($class) : self
|
||||
public static function fromClass($class, ? CacheInterface $cache = null) : self
|
||||
{
|
||||
return new static($class);
|
||||
return new static($class, null, $cache);
|
||||
}
|
||||
|
||||
public function read(bool $fullUses = true, bool $fullObject = true, $fullMethod = true, $fullProperty = true) : array
|
||||
{
|
||||
return [
|
||||
return $this->handleCaching(implode('', [ $this->classname, (int)$fullObject, (int) $fullMethod, (int) $fullProperty ]), fn() => [
|
||||
'uses' => $this->gatherUses($fullUses),
|
||||
'class' => $this->gatherClass($fullObject),
|
||||
'method' => $this->gatherMethods($fullMethod),
|
||||
'property' => $this->gatherProperties($fullProperty),
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public function gatherUses(bool $full = true) : array
|
||||
@@ -99,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;
|
||||
@@ -126,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 = [];
|
||||
@@ -148,7 +168,7 @@ class ObjectReflection {
|
||||
$parameters[$parameter->getName()] = [
|
||||
'null' => $parameter->allowsNull(),
|
||||
'position' => $parameter->getPosition(),
|
||||
'type' => $parameter->hasType() ? $parameter->getType()->getName() : false,
|
||||
'type' => $parameter->hasType() && $parameter->getType() instanceof \ReflectionNamedType ? $parameter->getType()->getName() : false,
|
||||
'array' => $this->isType('array', $parameter),
|
||||
'callable' => $this->isType('callable', $parameter),
|
||||
'optional' => $parameter->isOptional(),
|
||||
@@ -158,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,
|
||||
@@ -178,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +224,8 @@ class ObjectReflection {
|
||||
|
||||
protected function getUsesStatements() : array
|
||||
{
|
||||
return [];
|
||||
|
||||
$uses = [];
|
||||
$tokens = token_get_all( $c = $this->readCode() );
|
||||
|
||||
|
||||
+133
-34
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Notes;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
class ObjectResolver {
|
||||
|
||||
const KEY_ENTITY_NAME = 01;
|
||||
@@ -17,70 +19,152 @@ class ObjectResolver {
|
||||
|
||||
public array $methods;
|
||||
|
||||
public function __construct(string $objectClass, bool $fullUses = true, bool $fullObject = true, $fullMethod = true, $fullProperty = true)
|
||||
public function __construct(string $objectClass, bool $fullUses = true, bool $fullObject = true, $fullMethod = true, $fullProperty = true, ? CacheInterface $cache = null)
|
||||
{
|
||||
$this->objectClass = $objectClass;
|
||||
|
||||
list($this->uses, $this->class, $this->methods, $this->properties) = array_values(
|
||||
ObjectReflection::fromClass($objectClass)->read($fullUses, $fullObject, $fullMethod, $fullProperty)
|
||||
ObjectReflection::fromClass($objectClass, $cache)->read($fullUses, $fullObject, $fullMethod, $fullProperty)
|
||||
);
|
||||
|
||||
$this->resolveAnnotations();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transform an annotation into it's object's counterpart
|
||||
*/
|
||||
public function getAnnotationFromClassname(string $className) : ? object
|
||||
public function getAttributeFromClassname(array|string $className, bool $throwOnError = true) : object|bool
|
||||
{
|
||||
if ( $name = $this->uses[$className] ?? false) {
|
||||
foreach(array_reverse($this->class['tags']) as $item) {
|
||||
if ( $item['tag'] === $name ) {
|
||||
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 $item) {
|
||||
if ( $item['tag'] === $name ) {
|
||||
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 $item) {
|
||||
if ( $item['tag'] === $name ) {
|
||||
return $this->instanciateAnnotationObject($item);
|
||||
foreach ($this->methods as $method) {
|
||||
foreach ($method['tags'] as $tag) {
|
||||
if ($item['object'] instanceof $class) {
|
||||
return $this->instanciateAnnotationObject($tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new \Exception("Annotation `$className` could not be found within your object `{$this->objectClass}`");
|
||||
}
|
||||
else {
|
||||
throw new \InvalidArgumentException("Class `$className` was not found within {$this->objectClass} uses statement (or it's children / traits)");
|
||||
if ($throwOnError) {
|
||||
throw new \Exception(sprintf("Annotation `%s` could not be found within your object `%s`.", implode(', ', (array)$className), $this->objectClass));
|
||||
}
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an annotation into it's object's counterpart
|
||||
*/
|
||||
public function getAnnotationListFromClassname(string $className, bool $throwOnError = true) : array
|
||||
public function getAttributeListFromClassname(array|string $className, bool $throwOnError = true) : array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
if ( $name = $this->uses[$className] ?? false) {
|
||||
foreach($this->class['tags'] as $item) {
|
||||
foreach((array) $className as $class) {
|
||||
foreach ($this->class['tags'] as $item) {
|
||||
if ($item['object'] instanceof $class) {
|
||||
$list[] = $this->instanciateAnnotationObject($item);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->properties as $property) {
|
||||
foreach ($property['tags'] as $item) {
|
||||
if ($item['object'] instanceof $class) {
|
||||
$list[$property['name']] ??= [];
|
||||
$list[$property['name']][] = $this->instanciateAnnotationObject($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->methods as $method) {
|
||||
foreach ($method['tags'] as $item) {
|
||||
if ($item['object'] instanceof $class) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 ) {
|
||||
foreach ($this->properties as $property) {
|
||||
foreach ($property['tags'] as $item) {
|
||||
if ($item['tag'] === $name) {
|
||||
$list[$property['name']] ??= [];
|
||||
|
||||
$list[$property['name']][] = $this->instanciateAnnotationObject($item);
|
||||
@@ -88,9 +172,9 @@ class ObjectResolver {
|
||||
}
|
||||
}
|
||||
|
||||
foreach($this->methods as $method) {
|
||||
foreach($method['tags'] as $item) {
|
||||
if ( $item['tag'] === $name ) {
|
||||
foreach ($this->methods as $method) {
|
||||
foreach ($method['tags'] as $item) {
|
||||
if ($item['tag'] === $name) {
|
||||
$list[$method['name']] ??= [];
|
||||
|
||||
$list[$method['name']][] = $this->instanciateAnnotationObject($item);
|
||||
@@ -98,28 +182,43 @@ class ObjectResolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
}
|
||||
|
||||
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
|
||||
public function instanciateAnnotationObject(array $tagDefinition) : Annotation|\Notes\Attribute
|
||||
{
|
||||
if (isset($tagDefinition['object']) && $tagDefinition['object'] instanceof \Notes\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)");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -145,18 +244,18 @@ class ObjectResolver {
|
||||
protected function resolveAnnotations()
|
||||
{
|
||||
foreach($this->class['tags'] as $key => &$tag) {
|
||||
$tag['object'] = $this->instanciateAnnotationObject($tag);
|
||||
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
|
||||
}
|
||||
|
||||
foreach($this->properties as &$property) {
|
||||
foreach($property['tags'] as &$tag){
|
||||
$tag['object'] = $this->instanciateAnnotationObject($tag);
|
||||
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
|
||||
}
|
||||
}
|
||||
|
||||
foreach($this->methods as &$method) {
|
||||
foreach($method['tags'] as &$tag){
|
||||
$tag['object'] = $this->instanciateAnnotationObject($tag);
|
||||
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
){}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user