ANSR Framework is a simple web application framework, yet advocating enterprise practices. It's highly inspired from the major players in Java, .NET and PHP world, namely Spring, ASP.NET Core and Symfony.
You can route a web request to a specific class based function, when you decorate a function with the @Route
annotation in the docblock.
An automatic mapping of the request body to an object of certain class. No more manually handling the request (using superglobals is discouraged).
The framework starts and then it loads your application, putting it in it's own control. It might sounds scary, but this way you do not need to care about how and when objects are instantiated.
IoC leads to dependency injection. Whenever you need a certain business object, you just require it in the constructor or the function arguments. ANSR Framework will do its work.
If your application is classical server side rendered app, your actions (functions in your controllers) can return views with certain model. Remember, we do not advocate the "bunch of variables assigned to view" practice. They need to be encapsulated in a class.
If your application is a Web API then you can feel free to return the a model as JSON.
You need a common logic for a bunch of requests, for instance checking whether the user is logged in? No more need to invoke one and the same
method everywhere. Just write an interceptor to preHandle
the requests.
See an example of how the framework is used in src/DefaultApp
You need to set a single point of execution - index.php
and a folder for static resources web
. It depends on your web server.
There's already built in runtime configuration for apache httpd (.htaccess
), which can be used for testing purposes. Just copy the
framework folder somewhere in your document root and it will work.
Multiple application (bundles, modules, etc., ...) can reside in a single instance of the framework. What you need to do is as follows:
- Create a new folder with your application in
src
folder. This will be the rootnamespace of your app, for instance createForumApp
- Create a class named the same way (
ForumApp
) which extendsANSR\Core\FrameworkConsumer
- Go to
index.php
and inside the kernel booting callback:
$kernel->boot(function (\ANSR\Core\Application $app) {
});
Register your app using $app->registerApplication('ForumApp');
- You are ready. The framework will now scan your application to build dependency graph and check for routes that reside there.
A controller is nothing more than just a class which holds functions which accepts the HTTP Request and are responsible to return an HTTP Response.
An HTTP Response is everything that is a subtype of ANSR\Core\Http\Response\ResponseInterface.
Currently we support ViewResponse
, JsonResponse
, RedirectResponse
and a special internal type for the interceptor chain - ActionInterceptedResponse
.
Feel free to contribute with more subtypes. However, let's return to creating controllers.
- Create a folder inside
ForumApp
calledController
- Create a PHP Class inside
Controller
folder namedHomeController
- Make the class extends
ANSR\Core\Controller\Controller
so it can easily generate response and other handy functions will be there - Create a method named
index()
- Make it return an empty view response, e.g.:
public function index()
{
return $this->view();
}
- Import the
@Route
annotation usinguse ANSR\Core\Annotation\Type\Route;
- Decorate the method with this annotation using this docblock (write the following above the method definition)
/**
* @Route("/index", name="my_index")
*
* @return \ANSR\Core\Http\Response\ViewResponse
*/
- You are ready with the controller, but what does this mean?
@Route("index"
means that everytime the user wants to accessyoursite.com/index
the framework will instantiate your controller and invoke theindex()
method (passing it all arguments needed, if any, but that we will discuss later).name="my_index"
means the route is named. Everytime you want to refer to this route, you use the name, not the path. Paths may change, but we advise to not change names.@return \ANSR\Core\Http\Response\ViewResponse
is just a standard PHPDoc for what the function is returning. You can omit it, but smart IDEs likePHPStorm
will warn you that the@return
tag is missing.return $this->view()
- theview()
method comes from theController
you have already extended. It invokes the render method inViewInterface
which in its only implementation tries to create a response from aviewName
, if it's missing, it uses the controller name for a folder and the method name for a file, to find a view insideviews
folder in your app folder. So in this example it will try to find a filesrc/ForumApp/views/home/index.php
. There's no such file, so we can create it.
A view is nothing more than a simple php file which is included in the context of the ViewInterface::send() method. But since it's
included in this method, it means the file can benefit from the state of the method, having access to all of its local variables,
namely $model
(containing the information sent from the controller) and $view
(which is the ViewInterface
).
But now let's make it simple:
- Create a folder
views
in yourForumApp
directory - Create then
home
folder there - And finally create
index.php
in thehome
folder - Just omit all the contents of the file and use it like a simple HTML file which has one line
<h1>Hello World</h1>
- You are ready. Open
your.site/index
and you will seeHello World
in Heading 1 format
Suppose we have a register form. Hey, why not have one, instead.
- Create a controller
UsersController
the same way as the home one - Create a method
register()
which will render the form:
/**
* @Route("/register", name="register", method="GET")
*
* @return ViewResponse
*/
public function register()
{
return $this->view();
}
If you are wondering what is the attribute method="GET"
- this means that this method register()
will be invoke only if the
request method is HTTP GET. For any other method, it won't be invoked. We highly recommended separation from GET and POST, so we will use
another method for handling the form submission.
3. Create a view in src/ForumApp/views/users/register.php
which renders a form:
<h1>Register</h1>
<form action="<?=$view->url("register_handler");?>" method="post">
<div>
Username:
<input type="text" name="username"/>
</div>
<div>
Password:
<input type="password" name="password"/>
</div>
<div>
Confirm password:
<input type="password" name="confirmPassword"/>
</div>
<div>
<input type="submit" value="Register!"/>
</div>
</form>
If you wonder what the hell is <?=$view->url("register_handler");?>
you need to remember that view resides in the ViewResponse::send()
method
and has access to the $view
variable, which is ViewInterface.
If you remembered that we will not use paths, but names, now it should make it clear. The url()
method in ViewInterface
receives a name
of a route and build the url (optionally, if it has path variables, as well). So register_handler
will be our method that will handle
the form submission. It will be ocassionally on the same path, but this is not something the view should now.
- It's highly recommended to use model binding for form submissions. The form from above has three fields -
username
,password
andconfirmPassword
. Create a the following file and folders insrc/ForumApp
->Model/Form/User/UserRegisterRequestModel.php
:
class UserRegisterRequestModel
{
private $username;
private $password;
private $confirmPassword;
public function getUsername()
{
return $this->username;
}
public function setUsername($username)
{
$this->username = $username;
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getConfirmPassword()
{
return $this->confirmPassword;
}
public function setConfirmPassword($confirmPassword)
{
$this->confirmPassword = $confirmPassword;
}
}
- Now create a method in
UsersController
calledregisterHandler
which acceptsUsersRegisterRequestModel
and returns a view with it
/**
* @Route("/register", name="register_handler", method="POST")
*
* @param UserRegisterRequestModel $model
* @return ViewResponse
*/
public function registerHandler(UserRegisterBindingModel $model)
{
return $this->view($model);
}
- Create a view
src/ForumApp/views/users/registerHandler.php
with the following content
<?php /** @var \ForumApp\Model\Form\User\UserRegisterRequestModel $model */ ?>
<h1>You registered with</h1>
<h2>Username <?= $model->getUsername(); ?></h2>
<h2>Password <?= $model->getPassword(); ?></h2>
- Try to submit your form, it should print the request username and password
We do not provide an ORM. There's thin layer over PDO which provides separation between statements and result sets.
It resides here for MySQL and uses configuration for the connection.
You need to change the Variables::$args
array to export db.host
, db.user
and so on to one you needed
- You can inject
DatabaseInterface
object wherever you want (Controller's constructor or method signature) and start using it - But we highly recommend not to use database queries or any other application logic in the controllers. Leave them only for accepting requests and building responses.
- Use another layer for data access (e.g. repositories) - classes that solely do data access
- Create
src/ForumApp/Model/Entity/User.php
which has onlyusername
andpassword
fields - Create
src/ForumApp/Repository/UserRepositoryInterface.php
with the following content:
<?php
namespace ForumApp\Repository;
use ForumApp\Model\Entity\User;
interface UserRepositoryInterface
{
public function save(User $user);
}
- Create implementation
src/ForumApp/Repository/UserRepository.php
with the following content:
<?php
namespace ForumApp\Repository;
use ANSR\Driver\DatabaseInterface;
use ForumApp\Model\Entity\User;
class UserRepository implements UserRepositoryInterface
{
/**
* @var DatabaseInterface
*/
private $db;
public function __construct(DatabaseInterface $db)
{
$this->db = $db;
}
public function save(User $user)
{
$this->db->prepare("INSERT INTO users (username, password) VALUES (?, ?)")
->execute(
[
$user->getUsername(),
$user->getPassword()
]
);
}
}
- As we also highly recommen separation of the business logic from the data access, introduce a new layer of services for business logic.
Create
src/ForumApp/Service/UserServiceInterface.php
with the following content:
namespace ForumApp\Service;
use ForumApp\Model\Form\User\UserRegisterRequestModel;
interface UserServiceInterface
{
public function register(UserRegisterRequestModel $user);
}
- And its implementation
src/ForumApp/Service/UserService.php
with the following content:
<?php
namespace ForumApp\Service;
use ANSR\Core\Service\Encryption\EncryptionServiceInterface;
use ForumApp\Repository\UserRepositoryInterface;
use ForumApp\Model\Form\User\UserRegisterRequestModel;
class UserService implements UserServiceInterface
{
/**
* @var UserRepositoryInterface
*/
private $userRepository;
/**
* @var EncryptionServiceInterface
*/
private $encryptionService;
public function __construct(UserRepositoryInterface $userRepository, EncryptionServiceInterface $encryptionService)
{
$this->userRepository = $userRepository;
$this->encryptionService = $encryptionService;
}
public function register(UserRegisterRequestModel $user)
{
$this->userRepository->save(
new User(
$user->getUsername(),
$this->encryptionService->encrypt($user->getPassword())
)
);
return true;
}
}
As you might see, the application specific logic here is encrypting the password. This does not belong to the data access layer and also the actual RDBMS insert does not belong to the business layer, so they are now separated.
- Let's use the service in
registerHandler()
:
/**
* @Route("/register", name="register_handler", method="POST")
*
* @param UserServiceInterface $userService
* @param UserRegisterRequestModel $model
* @return RedirectResponse
*/
public function registerHandler(UserServiceInterface $userService, UserRegisterRequestModel $model)
{
if ($model->getPassword() != $model->getConfirmPassword()) {
$this->addFlash(SessionInterface::KEY_FLASH_ERROR, "Passwords mismatch");
return $this->redirectToRoute("register");
}
if ($userService->register($model)) {
$this->addFlash(SessionInterface::KEY_FLASH_SUCCESS, "Register successful");
return $this->redirectToRoute("login");
}
$this->addFlash(SessionInterface::KEY_FLASH_ERROR, "Username already taken");
return $this->redirectToRoute("register");
}
As you can see, since UserServiceInterface
is ANSR managed object and so does the UsersController::registerHandler
then the framework inject
the object so you just use it there.
addFlash
is a handy method from Controller
which creates flash messages - a message that persists only one request (after redirect).
We highly recommend Redirect after post
pattern.
- Now we need to change our register view a little bit to show the messages. Add before the form the following
<?php foreach ($view->getFlash('error') as $error): ?>
<div style="color: red">
Error: <b><?=$error;?></b>
</div>
<?php endforeach; ?>
getFlash()
gets you the bag inside a specific key, this time - error
. When it is empty (no one said the last request addFlash('error', 'some message')
the foreach will not trigger and no messages will be shown. Once they are shown, they are disposed, so after a refresh you won't see them.
Just to let you know, I've lied a little bit. There's kind of an ORM, but it's too loose for now. You can check it there: https://github.com/RoYaLBG/ANSR_Framework/tree/master/Core/Data
You can use the ActiveRecord from MySQLQueryBuilder
(only MySQL is supported now) to build queries instead of writing them manually.
Feel free to contribute with other QueryBuilders
- Every request hits one and the same script which acts like a front controller -
index.php
index.php
is responsible for a very certain work - to parse the HTTP Request and set basic information in order the application kernel to work- This basic information is as follows:
- Set up
http.host
variable - Set up
http.uri
variable - Instantiate one subtype of
ContainerInterface
, which will be used to store and locate dependencies - Instantiate the application kernel
\ANSR\Core\Kernel
giving the `ContainerInterface - Tell the Kernel what to do once the application starts (you can add more things here, now it says the kernel to load some of crucial objects needed for the application as dependencies such as annotation parsers, routers, query builders, autoloaders and so on)
- Eventually tell the kernel to add dependencies with higher priority than the annotations using
overrideAnnotationConfiguration()
method - Of course, boot the kernel
- The kernel now boots, which means
- Locate the
Application
with all its dependencies using theContainerInterface
- Execute the delegate passed from
index.php
e.g. register applications (e.g.ForumApp
) - Pass the control to the
ANSR\Core\Application
instance viastart(WebApplicationProducerInterface)
method
- The application now prepares to start
- The application highly relies on configuration through annotations so it invokes the
process
method ofAnnotationProcessorInterface
- This procedure scans all class files in the framework including registered apps for annotations in the docblock
- An annotation in the docblock is a special
@tag
that is a valid class, subtype ofANSR\Core\Annotation\AnnotationInterface
- For this type checking is responsible
ANSR\Core\Annotation\Parser\AnnotationParseInterface
which parses the docblock and gives the control toAnnotationTokenInterface
- The token interface produces
AnnotationBuilderInterface
so additional data can be set to the annotation (e.g. property such asmethod
inRoute
; orAnnotatedFileInfo
) - Any annotation is just metadata and does not execute logic by itself. It's either used by an external processor or by a strategy
- The annotation processor is responsible for executing different execution strategies which already created by
AnnotationExecutionStrategyFactoryInterface
- For instance, once it parses a
@Route
annotation, it finds by conventionANSR\Core\Annotation\Strategy\RouteExecutionStrategy
which is responsible for parsing the URL for instance (as the URL might be a template e.g./users/{id}/books
) so it should parse all path variables and create a map with routing templates for later usage (theRouter
will use this mape). But that's a costly operation to scan all over the files, so only the first request ever scans for annotations, then each strategy creates a cache (files cache) in/cache/
folder e.g./cache/routes.php
where is stored the map with routes in the format:
REQUEST_METHOD => {
'template' => {
PATH => { 'controller' => '...', 'action' => '...', 'args' => [..], 'name' => '..' }
},
'name' => {
NAME_ROUTE => { 'controller' => '...', 'action' => '...', 'args' => [..], 'template' => '..' }
}
}
- So either things are in the cache, or all execution strategies are executed, the control is given back to the
Application
class - The
Application
now tells theContainerInterface
to load all its dependencies from the cache/cache/components.php
. This file was populated, because from the above logic - the@Component
annotation was found, itsComponentExecutionStrategy
populated the cache with a map of
INTERFACE => {
PRIORITY => IMPLEMENTATION
}
- The container loads them by
highest priority
in its internal map and gives the control back toApplication
- Application uses the producer
WebApplicationProducerInterface
that accepted as an argument in order to produce an object ofWebApplication
- Then it passes the control to the
start()
method ofWebApplication
WebApplication::start()
is responsible for calling theRouterInterface
to dispatch the web request to a certain function, effectively calling it and getting its returnedResponseInterface
- The
dispatch()
method in the currentDefaultRouter
implementation is a bit complex - First it loads the routes from the cache
- Then it scan all the routes to match the request against them and find the most suitable one
- If the request does not match any route, it loads default route
- If no route is found at all, it raises an exception
- If route is found, it now proceeds to scan the metainfo of the function to be execution
- For instance, is there an authorization metainformation (which was taken from the
@Auth
annotation and put in to the cache) - If there is, it first asks the
AuthenticationServiceInterface
if the userisLogged()
, if it is not, the router producesRedirectResponse
to the login page - If the user is logged in, it checks in the
@Auth
info whether certain roles are needed. If there are, it asksAuthenticationServiceInterface
whether the userhasRole(role)
- If the user does not have the roles, the procedure is the same - a
RedirectResponse
is returned from thedispatch()
method - If the user is logged in and has the needed roles, or no authentication or roles are needed, the
dispatch()
method proceeds - It proceeds to instantiate the controller, using the
ContainerInterface::resolve
method - The
resolve(abstraction/implementation
method recursively build up the dependencies as they might need dependencies deeply - Once the controller is instantiated, the
dispatch()
method needs to call the method - But the method might accept arguments: either path variables (e.g.
{id}
), form binding models or services) - There are a few rules here:
- If the argument is primitive - then it's a path variable
- If the argument is complex object, it asks the container
- If the container has this object - resolve it
- Otherwise try to build form binding model
- Once the function is resolved, the
dispatch()
method checks whetherInterceptors
needs to be executed - If needed, it builds
InterceptorChain
and returnsActionInterceptedResponse
- If not, it executes the
Controller::action
with the parameters from35.
which producesResponseInterface
and returns it - Action back to
WebApplication
which received the returned response fromdispatch()
WebApplication
then executesResponseInterface::send()
method and the response is sent back to the user. Whoala - magic done!