Commit 1f001c6c authored by Avris's avatar Avris
Browse files

init

parents
Pipeline #946242 skipped
tests/_output/*
vendor/*
.idea/*
**/.DS_Store
## Micrus Annotations ##
This is a module for [Micrus framework](http://micrus.avris.it) that allows you to put some controller cofiguration straight inside the controllers.
To install this module, open the file `app/Config/modules.yml` and add:
- Avris\Micrus\Annotations\AnnotationsModule
Then run:
composer require avris/micrus-annotations
### @M\Route ###
Generally, route definitions are put in the `app/Config/routing.yml` file, like this:
postList:
pattern: /post/list
controller: Post/list
postRead:
pattern: /post/{id}/read
controller: Post/read
requirements: { id: \d+ }
postAdd:
pattern: /post/add
controller: Post/form
postEdit:
pattern: /post/{id}/edit
controller: Post/form
requirements: { id: \d+ }
When this module is enabled, you could put that information directly in the controller:
<?php
namespace App\Controller;
use Avris\Micrus\Controller\Controller;
use Avris\Micrus\Annotations as M;
/**
* @M\Route("/post")
*/
class PostController extends Controller
{
/**
* @M\Route("/list")
*/
public function listAction()
{
// ...
}
/**
* @M\Route("/{id}/read", requirements={"id"="\d+"})
*/
public function readAction(Post $post)
{
// ...
}
/**
* @M\Route("/add", name="postAdd")
* @M\Route("/{id}/edit", name="postEdit", requirements={"id"="\d+"})
* @M\Secure(check="canEditPost")
*/
public function formAction(Post $post = null)
{
// ...
}
}
Note that:
* a `use` statement is required,
* when all your routes in some controller share the same prefix, you can declare it as a class annotation,
* when no `name` is declared, it will be set to `controller name` + `action name`, eg. `postList`,
* multiple routes can be attached to one controller.
### @M\Secure ###
Analogously, authorisation config can also be moved from `app/Config/security.yml` to the annotations:
security:
public:
- { pattern: ^/post/restricted/public$ }
restrictions:
- { pattern: ^/post/restricted }
- { pattern: ^/post/admin, roles: [ROLE_ADMIN] }
- { pattern: ^/post/add$ }
- { pattern: ^/post/(\d+)/edit$, check: canEditPost }
Will become:
<?php
namespace App\Controller;
use Avris\Micrus\Controller\Controller;
use Avris\Micrus\Annotations as M;
class PostController extends Controller
{
/**
* @M\Secure
*/
public function restrictedAction()
{
// ...
}
/**
* @M\Secure(public=true)
*/
public function restrictedPublicAction()
{
// ...
}
/**
* @M\Secure(roles={"ROLE_ADMIN"})
*/
public function adminAction()
{
// ...
}
/**
* @M\Secure(check="canEditPost")
*/
public function formAction(Post $post = null)
{
// ...
}
}
{
"name": "avris/micrus-annotations",
"type": "library",
"description": "Annotations for the Micrus Framework",
"license": "CC-BY",
"homepage": "http://micrus.avris.it",
"authors": [{
"name": "avris",
"email": "andrzej@avris.it",
"homepage": "http://avris.it"
}],
"require": {
"avris/micrus": "^2.0",
"doctrine/annotations": "^1.2"
},
"autoload": {
"psr-4": { "Avris\\Micrus\\Annotations\\": "src" }
}
}
<?php
namespace Avris\Micrus\Annotations;
use Avris\Micrus\Controller\Routing\RoutingExtension;
use Avris\Micrus\Tool\Cache\Cacher;
use Avris\Micrus\Tool\SecurityExtension;
use Doctrine\Common\Annotations\AnnotationRegistry;
class AnnotationsExtensions implements RoutingExtension, SecurityExtension
{
/** @var string */
protected $dir;
/** @var AnnotationsLoader */
protected $loader;
public function __construct($dir, Cacher $cacher)
{
AnnotationRegistry::registerFile(__DIR__ . '/Route.php');
AnnotationRegistry::registerFile(__DIR__ . '/Secure.php');
$this->loader = $cacher->cache('annotations', function($dir) {
return new AnnotationsLoader($dir);
}, [realpath($dir)]);
}
public function getRouting()
{
return $this->loader->getRoutes();
}
public function getRestrictions()
{
return $this->loader->getRestrictions();
}
public function getPublicPaths()
{
return $this->loader->getPublicPaths();
}
}
<?php
namespace Avris\Micrus\Annotations;
use Doctrine\Common\Annotations\AnnotationReader;
class AnnotationsLoader
{
/** @var AnnotationReader */
protected $reader;
/** @var array */
protected $routes = [];
/** @var array */
protected $publicPaths = [];
/** @var array */
protected $restrictions = [];
public function __construct($dir)
{
$this->reader = new AnnotationReader();
$it = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
/** @var \SplFileInfo $file */
foreach ($files as $file) {
if ($file->isFile()) {
$filename = ltrim(substr($file->getPath(), strlen($dir)) . '/' . $file->getFilename(), '/');
if ($controllerName = $this->testEnding($filename, 'Controller.php')) {
$this->loadAnnotationsForController(str_replace('/', '\\', $controllerName));
}
}
}
}
protected function loadAnnotationsForController($name)
{
$routeBase = '';
$class = new \ReflectionClass('App\Controller\\' . $name . 'Controller');
foreach ($this->reader->getClassAnnotations($class) as $ann) {
if ($ann instanceof Route) {
$routeBase = $ann->getPattern();
} elseif ($ann instanceof Secure) {
if ($ann->isPublic()) {
$this->publicPaths[] = $ann->getData($name);
} else {
$this->restrictions[] = $ann->getData($name);
}
}
}
foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if ($actionName = $this->testEnding($method->getName(), 'Action')) {
foreach ($this->reader->getMethodAnnotations($method) as $ann) {
$controller = $name . '/' . $actionName;
if ($ann instanceof Route) {
$routeName = $ann->getName() ?: $this->buildRouteName($controller);
$this->routes[$routeName] = $ann->getData($routeBase, $controller);
} elseif ($ann instanceof Secure) {
if ($ann->isPublic()) {
$this->publicPaths[] = $ann->getData($controller);
} else {
$this->restrictions[] = $ann->getData($controller);
}
}
}
}
}
}
protected function buildRouteName($controller)
{
if ($controller === 'Home/home') {
return 'home';
}
if (preg_match('#^(.*)Controller/(.*)#', $controller, $matches)) {
$controller = $matches[1] . '_' . $matches[2];
}
return lcfirst(preg_replace_callback(
'/[\\\\\/\-][a-zA-Z]/',
function($matches) { return strtoupper($matches[0][1]); },
$controller
));
}
protected function testEnding($string, $ending)
{
return substr($string, strlen($string) - strlen($ending)) == $ending
? substr($string, 0, strlen($string) - strlen($ending))
: false;
}
/**
* @return array
*/
public function getRoutes()
{
return $this->routes;
}
/**
* @return array
*/
public function getPublicPaths()
{
return $this->publicPaths;
}
/**
* @return array
*/
public function getRestrictions()
{
return $this->restrictions;
}
public function __sleep()
{
return ['routes', 'publicPaths', 'restrictions'];
}
}
<?php
namespace Avris\Micrus\Annotations;
use Avris\Micrus\Module;
class AnnotationsModule implements Module
{
public function extendConfig($env, $rootDir)
{
return [
'services' => [
'annotationsExtensions' => [
'class' => __NAMESPACE__ . '\AnnotationsExtensions',
'parameters' => ['{@rootDir}/app/Controller', '@cacher'],
'tags' => ['routingExtension', 'securityExtension'],
],
],
];
}
}
<?php
namespace Avris\Micrus\Annotations;
/**
* @Annotation
* @Target({"CLASS", "METHOD"})
*/
class Route
{
/** @var string */
protected $pattern;
/** @var string */
protected $name;
/** @var array */
protected $options = [];
public function __construct($values)
{
$this->pattern = isset($values['value']) ? $values['value'] : $values['pattern'];
if (isset($values['name'])) {
$this->name = $values['name'];
}
foreach (['defaults', 'requirements', 'methods', 'unrestricted'] as $field) {
if (isset($values[$field])) {
$this->options[$field] = $values[$field];
}
}
}
/**
* @return string
*/
public function getPattern()
{
return $this->pattern;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
public function getData($base, $controller)
{
return array_merge([
'pattern' => $base . $this->pattern,
'controller' => $controller,
], $this->options);
}
}
<?php
namespace Avris\Micrus\Annotations;
/**
* @Annotation
* @Target({"CLASS", "METHOD"})
*/
class Secure
{
/** @var array */
protected $options = [];
/** @var bool */
protected $public = false;
public function __construct($values)
{
if (isset($values['public']) && $values['public']) {
$this->public = true;
}
foreach (['pattern', 'roles', 'check'] as $field) {
if (isset($values[$field])) {
$this->options[$field] = $values[$field];
}
}
}
public function getData($controller)
{
return array_merge([
'controller' => $controller,
], $this->options);
}
public function isPublic()
{
return $this->public;
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment