Layer is an Object Oriented MVC PHP Framework
This project is currently on ALPHA phase, you can still try it and give me your feedback
MVC stands for Model-View-Controller
The model represents the data to manipulate, the view represents how the data should be displayed, the controller handles the business logic of your app.
-
Models are defined by simple classes
-
Views are defined by .php file with HTML content
-
Controllers are defined by classes
-
Actions are defined by methods inside the controllers
You can easily create API or website with Layer
-
A website uses views to display content and let the user interact with it (MVC)
-
An API does not uses views to display the data, it return the data in a specific format like JSON, XML, Text...
-
You can define custom errors controller for your website or api
Get the latest version from master by running this command after your project initialisation
composer require rloris/layer:dev-master
If you update layer, delete the generated files (map.json, routes.json, hash.json) to get the latest changes
First we need to setup our configuration.json file, it should look like this, it's the minimum setup
{
"locations": {
"controllers": "{path_to_the_folder_containing_your_controllers}",
"shared": "{path_to_the_folder_containing_your_shared_components}",
"build": "{path_to_the_folder_for_layer_output_build_files}",
"log": "{path_to_the_folder_for_layer_output_log_files}"
},
"environment": {
"current": "dev",
"dev": {
"routeTemplate" : "",
"apiRouteTemplate" : "api",
"log": false,
"logTemplate" : "[{request_datetime}][{environment}][{request_method} {request_resource}]:{message}",
"build": true
},
"prod": {
"routeTemplate" : "",
"apiRouteTemplate" : "api",
"log": true,
"logTemplate" : "[{request_datetime}][{environment}][{client_ip} {client_browser} {client_os}][{request_method} {request_resource}]:{message}",
"build": false
},
"{your_other_env_name}": {
"routeTemplate" : "",
"apiRouteTemplate" : "api",
"log": false,
"logTemplate" : "[{request_datetime}][{environment}][{client_ip} {client_browser} {client_os}][{request_method} {request_resource}]:{message}",
"build": false
}
},
"layouts" : {
"{your_layout_name}": {
"pre": ["header","navbar","breadcrumbs"],
"post": ["footer"]
},
"{your_other_layout_name}": {
"pre": ["header", "navbar"],
"post": ["footer"]
}
},
"globals": {
"{name_of_the_constant}": "{value_of_this_constant}",
"{name_of_another_constant}": "{value_of_this_constant}",
}
}
locations
controllers
points to the directory containing all the controllers of your applicationshared
points to the directory containing all the shared elements (views, filters) of your applicationbuild
specify the directory where layer will be generating files used by the frameworklog
points to the directory where log files will be stored
environment
routeTemplate
is the prefix that will be added for all Controller routesApiRouteTemplate
is the prefix that will be added for all ApiController routes (For example: useful for api errors to show them as json instead of html view)log
enable/disable LoggerlogTemplate
Tells to the Logger the structure to use for each log entrybuild
enable/disable scan for changes in folder (controllers, shared) to build the new routes map (recommended: set true in development env and false in production)
layouts
- Layouts are shared views attached together, they are used only for a website, not for an api, leave empty if you are building an api
- keys are the names of your layouts, you can use layouts in Controllers for your actions by specifying the layout to use
- Layouts are made out of
pre
views (will be rendered before your main content) andpost
views (will be rendered after your main content) - You change the layout configuration at runtime thanks to the viewManager available in Controllers
globals
- You can store whatever you want in the
globals
section - All keys will be converted in uppercase constant, for example the value of
my_key
will be available inMY_KEY
in the whole app
The structure is organized like this, you have folders with the name of the controller and inside you can find the controller php file, if it's a website controller, you can also find the views folder that contains the views used by this controller.
NOTE: A specific controller can only use views from it's own views folder or shared views, it cannot uses views from another controller views folder. If you want to reuse a view for different controllers, create a shared view !
/mycontrollers
/home
HomeController.php
/views
index.php
contact.php
about.php
/blog
BlogController.php
/views
list-post.php
edit-post.php
/auth
AuthController.php
/views
login.php
signin.php
/api
/user
UserApiController.php
/blog
BlogApiController.php
/shared
/filters
AuthFilter.php
LogFilter.php
GlobalFilter.php
/views
header.php
modal.php
footer.php
/alerts
success.php
warning.php
failure.php
This will be the main entry point of your application, every request will trigger this script, you should not display anything in it but you can add code before and after for other reasons.
// require autoloader
require_once "./vendor/autoload.php";
// init app with configuration file path
$app = rloris\layer\App::getInstance("./configuration.json");
// execute app
if($code = $app->execute())
{
// request or error handled successfully
rloris\layer\utils\Logger::write("Serving content successfully with status code: $code");
}
else
{
// error could not be handled
rloris\layer\utils\Logger::write("Error occurred");
}
All requests should be forwarded to your index.php entry file
<IfModule mod_rewrite.c>
Options +FollowSymLinks
RewriteEngine On
RewriteCond %{REQUEST_URI} !-d
RewriteCond %{REQUEST_URI} !-f
RewriteCond %{REQUEST_URI} !-l
RewriteRule ^(public)($|/) - [L]
RewriteRule ^(.*)$ index.php?url=$1 [L,QSA]
</IfModule>
This redirects all request to the index.php file or public folder where your css, images, js will be located
location -d {}
location -f {}
location -l {}
location ~ ^/(public)($|/) { }
location / {
rewrite ^(.*)$ /index.php?url=$1 break;
}
All requests should be forwarded to your index.php entry file or public folder where your css, js, images... are located
Once your setup is done you can begin to create controller classes, view files and filter classes by following the instructions in the next chapter. Layer works with annotations on classes and methods, it automatically builds the routes map, no need to tell the router to add a route. Layer detects if a file was updated or not by using hashes to only rebuild the updated part.
You should NOT display anything in your controllers, but pass this content to the view if you want to display it. WHY ? because headers are handled and sent by Layer, by displaying something in the controllers or filters you break this system and your custom layer headers won't be sent.
Layer detects a controller if it's in the controllers folder specified in the configuration.json file, and that the controller's class name contains {Your_Name}Controller.php
, once your class is created just extends it with your specific needs:
- BaseController for a website controller
- ApiBaseController for an api controller
- ErrorBaseController for a website error controller
- ApiErrorBaseController for an api error controller
Then add an annotation to tell layer how it should handle this controller's route :
Put these on top of an ApiBaseController class @ApiController
/**
* @ApiController(routeTemplate='users', defaultAction='getUsers')
*/
class UserApiController extends ApiBaseController { ... }
This means when you visit /api/users/
the default action is getUsers()
You do not need to add /api
because it is already done by reading the configuration.json "apiRouteTemplate" key in environment/{current_env}/apiRouteTemplate
If you change this key in your configuration file to myApi
then all api routes will be available by visiting /myApi/{route}
Then for each public method in this class you want to reach add this annotation @ApiAction
// inside UserApiController class
/**
* @ApiAction(routeTemplate='/',methods={'get'})
*/
public function getUsers() { ... }
/**
* @ApiAction(routeTemplate='/',methods={'options'})
*/
public function getOptions() { /* Handle CORS */ }
The getUsers() action will be triggered when you visit /api/users/
with GET method
Put these on top of a BaseController class @DefaultController
or @Controller
Put @DefaultController
on top if it's the controller that will be triggered when there is not route specified (default route)
/**
* @DefaultController(routeTemplate='home', layoutName='basic')
*/
class HomepageController extends BaseController { ... }
If I visit /
my request will be forwarded internally to the HomepageController
, I can also visit /home
to reach this controller, there should be only one @DefaultController
in your project, the others should be @Controller
The default action for @Controller
and @DefaultController
is index(), you can of course change it by specifying another one in these annotations
Then for each public method in this class you want to reach add this annotation @Action
// inside HomepageController class
/**
* @Action(methods={"get"})
*/
public function index() { ... }
/**
* @Action(methods={"get"})
*/
public function about_us() { ... }
/**
* @Action(methods={"get"})
*/
public function contact() { ... }
When you do not specify the routeTemplate in the annotation, the method name will be used, thus by visiting /home/about_us
or /home/contact
the about_us() or contact() action will be triggered
Put these on top of an ErrorBaseController class @ErrorController
/**
* @ErrorController(layoutName='basic')
*/
class ErrorsController extends ErrorBaseController { ... }
This tells layer to use this class to handle all errors thrown by a website
Then inside this controller, put this annotation on top of methods to handle specific errors ``
// inside ErrorsController class
/**
* @ErrorAction(errorCodes={"5\d\d"}, viewName='index')
*/
public function serverError() { ... }
/**
* @ErrorAction(errorCodes={"404", "400"}, viewName='index')
*/
public function notFoundError() { ... }
/**
* @ErrorAction(viewName='index')
*/
public function clientError() { ... }
In this case, all errors with an http code of 5xx
will trigger serverError()
, all errors with an http code of 404
and 400
will trigger notFoundError()
, the rest will trigger clientError()
Put these on top of an ApiErrorBaseController class @ApiErrorController
/**
* @ApiErrorController
*/
class ApiErrorsController extends ApiErrorBaseController { ... }
This tells layer to handle all api errors and forward them to this controller
Then simply tell layer how to handle a specific error by specifying the action to trigger, put this annotation on method inside this controller @ApiErrorAction
// inside ApiErrorsController class
/**
* @ApiErrorAction(errorCodes={"4\d\d"})
*/
public function clientError() { ... }
/**
* @ApiErrorAction(errorCodes={"5\d\d"})
*/
public function serverError() { ... }
In this case all errors with an http code of 5xx
will trigger the serverError()
action and all errors with http code of 4xx
will trigger the clientError()
action
Layer handles route parameters by name, you can define parameters in routeTemplate (controller and action) like this :
/**
* @Controller(routeTemplate='auth/{#identifier}', layoutName='basic', filters={'time'})
*/
class AuthController extends BaseController { ... }
Here I have declared in my AuthController a mandatory parameter of type number, this means all my actions will need this parameter to be triggered, and of course, they will be able to get it
If your parameter is mandatory and can be anything, the syntax to declare it, is this one, {param}
If your parameter is mandatory and a number, the syntax to declare it, is this one {#param}
If you parameter is not mandatory, just add ?
at the end like this whether it's a number or other {param?}
Then to get this parameter, just use it's name as method parameter like this :
// inside AuthController class
/**
* @ApiAction(methods={"get"})
*/
public function connect($identifier) { ... }
As you notice here, I set up the routeTemplate of the controller thus I can use $identifier
for all methods inside this controller, if you want the same result only for an action, do it like this :
/**
* @Controller(routeTemplate='auth', layoutName='basic', filters={'time'})
*/
class AuthController extends BaseController {
/**
* @ApiAction(routeTemplate='{#identifier}', methods={"get"})
*/
public function connect($identifier) { ... }
}
Here the difference is that only connect method will be able to use $identifier
since I didn't declare it for the controller
Filters are actions that will be executed before and/or after the main action, filters are classes, they are useful to test if a user is connected before accessing to a specific resource or simply log something
You can apply filters on controller or apiController (this means it will be applied to all actions inside this controller) or on some actions only if you specify it
Layer detects a filter if it's in the shared folder specified in the configuration.json file, and that the filter's class name contains {Your_Name}Filter.php
, once your class is created just extends it with the BaseFilter
class.
Then, add this annotation : @Filter
eg:
/**
* @Filter
*/
class AuthFilter extends BaseFilter
{
public function in()
{
/* input stuff here */
}
public function out()
{
/* output stuff here */
}
}
You can use a filter on multiple actions or controllers, the order defines which filter will be called first, like this :
- To apply a filter on a controller (thus every action in this controller)
/**
* @DefaultController(filters={'time', 'log'})
*/
class HomepageController extends BaseController { ... }
In this case, on input, the filter called time will be called first then log, then on output, log will be called first then time.
- To apply a filter on an action
/**
* @Action(methods={"post"}, filters={"auth"})
*/
public function upload()
{
self::$data = ["content" => "File uploaded with success", "title" => 'Upload file'];
}
If upload is inside the HomepageController, then filters will be applied in this order, on input,
time => log => auth
then the upload() method will be called, and on output,
auth => log => time
You can define Global Filters by changing the annotation to @GlobalFilter
, they will be applied every time :
/**
* @GlobalFilter
*/
class MyGlobalFilter extends BaseFilter
{
public function in()
{
// CORS allows all origins for api
if(strtoupper(self::$request->getRequestMethod()) === IHttpMethods::OPTIONS) {
self::cors()->allowAnyOrigins()->allowAnyMethods()->allowAnyHeaders();
}
}
public function out()
{
/* output stuff here */
}
}
You can add and remove filter at runtime thanks to the filterManager available in Controller classes and Filter classes :
self::$filterManager->add("auth");
if(self::$filterManager->isActive("time"))
self::$filterManager->remove("time");
Views are php files that contains mainly HTML with a few php code, but you should not put your business logic there, to pass content to the view, it's the same way you send data for an api, just add your content to self::data[yourkey] = content;
in your controller or filter and then access it like this <?= $this->yourkey ?>
, if the content you are accessing in your view does not exists, it won't display anything and will not throw an error.
You can add and remove views at runtime thanks to the viewManager available with self::$viewManager
by extending the class BaseController
.
PreViews
are shared views that will be displayed before the main content
PostViews
are shared views that will be displayed after the main content
ContentView
is the main content view that will be displayed after the previews and before the postviews
// getting GET parameter with name ok
$ok = self::$request->getGet('ok');
// adding alert shared view before main content of this action
if($ok === 'true')
{
self::$viewManager->addPreView('alerts/success');
}
else if($ok === 'false')
{
self::$viewManager->addPreView('alerts/failure');
}
else
{
self::$viewManager->addPreView('alerts/alert');
}
You can access the request in any controller or filter class by typing self::$request
DOC TODO
You can access the response in any controller or filter class by typing self::$response
DOC TODO
The file API allows you to handle a file very easily whether it's an uploaded file or not
DOC TODO
You can access the Session manager in any controller or filter class by typing self::session()
DOC TODO
You can access the CORS manager in any controller or filter class by typing self::cors()
DOC TODO
You can use the logger to write directly using a specific template from your configuration file Logger::write("Hello World")
DOC TODO
- Add XML output support for API response
- Add route parameters customization with annotation like
@RouteParam(name="id", regex="...")
support - Add websocket server support => (extension ?)
- Add LORM (Layer Object Relational Mapping) support => (extension ?)