Load control files in path, using section controllers if they are found
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.
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
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(){
}
}
There are four variations that a page control can take:
In Controller, within the section Controller.php, a method can correspond to the page name function page1(){}
Parellels section controller class, but is named for the page. /page1
-> class page1{}
Can return a closure. The closure will have parameters injected.
The page itself can just do stuff. It can still return something - like the output of a template.
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){}
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.
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]);
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
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
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.
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.
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.
- 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
APIs + response handlers are good for handling standards. But, a controller can output anything, and sometimes a large variance from a standard is necessary.
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: