grithin / control-path
Load a series of control files based on path
Requires
- php: >=5.3.0
- grithin/file-include: ^0.1.0
- grithin/ioc-di: ^0.5.0
Requires (Dev)
- grithin/phpbase: ^5.0
- grithin/phpunit: ^0.1.0
This package is auto-updated.
Last update: 2024-10-22 22:34:38 UTC
README
Load control files in path, using section controllers if they are found
Why
The router I previously built would load control files parallel to a requested url path. It included section control files that acted like frontware, and it provided an intuitive location for control files. But, it had some downsides:
- variable injection was limited to a preset array
- could not bundle multiple pages into a single controller class which might have utility methods for the pages and a __construct
I also considered that standard Controller class paradigm had downsides.
So, I resolved to have the best of both worlds and made this tool.
What
ControlPath loads section and page controls. The returns from the controls are collected, and they form the array that is returned from the load
function.
use \Grithin\ServiceLocator; $sl = new ServiceLocator(); $cp = new ControlPath(__DIR__.'/test_files/app1/', $sl->injector_get()); $returns = $cp->load('/section1/page');
How to interpret these returns is up to the application. For instance, the controls could return a response object, or a string which something else turns into a response object.
Let's look at an example.
With a path of /section1/page
, the steps a flow goes through while($Flow->next() !== false)
are:
- .
- load
/Controller.php
$Controller = new Controller()
$Controller->_always()
- load
- .
- load
/section1/Controller.php
$Controller = new section1\Controller()
$Controller->_always()
- load
- .
- load
page.php
- load
Section Control
section1\Controller.php
namespace App\Control\section1; class Controller{ # always called, but return is not used public function __construct(){ } # always called, return is used public function _always(){ } }
Page Control
There are four variations that a page control can take:
Section Class Method
In Controller, within the section Controller.php, a method can correspond to the page name function page1(){}
Page Class
Parellels section controller class, but is named for the page. /page1
-> class page1{}
Closure
Can return a closure. The closure will have parameters injected.
Page Logic
The page itself can just do stuff. It can still return something - like the output of a template.
Injection
The methods, closures, and __constructs have normal dependency injection. Additionally, some parameters are injected based on the name of the parameter. The named injections are based on the inject
option. Two variables are always injected:
Flow
the current Flow instancecontrol
and ArrayObject used for sharing data between controlsControlPath
the current ControlPath instance
You can have these injected just by name:
# within a method/constructor function __construct($ControlPath){} # within a closure return function($ControlPath){}
Sharing
It is intended that section controls and page control may want to share data. To enable this, the $share
variable, which is an ArrayObject, is injected. This can also be used to capture methods within a Controller class.
Controller.php
class Controller{ function __construct($share){ $share->do_x = function(){ $this->do_x(); }; } protected function do_x(){ echo 'bob'; } }
page.php
($share->do_x)();
Whereas, normally, a section Controller would have protected methods that it shares amongst the page methods within the class, if a section controller wanted to provide access to those methods to either page controls that weren't methods, or page controls that were deeper, it would have to do something like the above.
If necessary, the previous Controller classes can be access with $Flow->controllers
. This allows accessing public data (object properties) on the Controller that might be useful. However, owing to the fact that public methods with the Controller are accessible via path, only pages (and builtins) should be public methods, and utility methods should be protected.
Stopping Control Flow
There are a few ways to stop control flow:
- return false from a control
- call $Flow->stop()
- throw exception
Where #2 occurs can be in multiple places. Since $Flow
is injected:
- within a section __construct
- within a section _always
- within a page control file
- within a return_handler
Controller.php
class Controller{ public function __construct($Flow){ $Flow->stop(); } }
Return handler
$return_handler = function($return, $flow){ # there can be no "bill"'s, only bob's if($return == 'bill'){ $flow->stop(); } } $ControlPath->load('/section1/page', ['return_handler'=>$return_handler]);
Conforming Path Tokens
Since path characters can diverge from what is allowable as a namespace or class, the token parts of the path are converted using ControlPath::token_to_class.
'.-' are used as separated, non-conforming characters are removed, and the token is turned into camel case:
029bob.bill-sue1
=> bobBillSue1
Exceptions
Grithin\ControlPath\NotFound
: when a page is not found, or when a path is directed to a built inGrithin\IoC\ContainerException
: when injection can not resolve the service for a typed paramaeterGrithin\IoC\MissingParam
: when injection can not resolve a param
Misc Design Notes
Class Files Vs Logic Files
Multiple load
's become a problem when there is a mixture of class files and logic files. You can not re-include a class file, but a logic file expects to be re-included. As such, when ControlPath discoveres a class file, it will not try to load it again on subsequent runs.
ServiceLocator singleton ControlPath
There is a question of whether the ControlPath should clone the ServiceLocator or not. ControlPath adds itself to the SL. By having a cloned SL, it ensures if a control file calls SL->get(ControlPath), this will always points to the instance loading the control file. However, this creates a mismatch of expectation:
- A control file might add a middleware, and it might add a service for that middleware that will be injected, but because the ServiceLocator the control file is using is a clone, that service will not be registered in the ServiceLocator that loads the middleware.
In the end, I decided not to clone ServiceLocater. ControlPath can be injected by name instead of by type, which means control files only need to name the param $ControlPath
to ensure they are getting the current instance.
Why is Route Middleware Deficient?
Generally, there are complexities that apply to specific paths that would make the abstraction into front/middle ware both unnecessarily disconnected and would result in unnecessarily complex general middleware or route specific frone/middle ware. One off frontwares, indicated by path, fit perfectly for these path specific complexities, to be loaded as section controls. And example might be some series of complex permission checks which would not fit in a Auth middleware configuration parameter.
Controller Class Downsides
- Giant methods or manied methods lead to giant, unmanageable controller files.
- large control pages would have to fit inside a Controller class, making the class large
- buried pages
- page methods in no definite order
- Page syntax error causes section failure
Why Not Controller As Api?
APIs + response handlers are good for handling standards. But, a controller can output anything, and sometimes a large variance from a standard is necessary.
Plans
Setting this up into a framework takes some effort.
Having something like laravel with route methods (Model $model)
would mean injecting Model(Request $request)
, which would mean Request is a service or is otherwise injectable at that point.
Fitting ControlPath in with other wares, where there is the potential section controls will add more wares is also complicated: