I've created a base structure for my project and found a way to wire classes together, I create an array of previously loaded classes and call them from the cache. I then have a static class that holds the cache and I call the static class when I need a class.
I feel that there is a much better way to do this, and maybe I'm doing something extreemly wrong? How do other PHP applications do this? I'm pretty new to it, so I was wondering if anyone could advise me towards a better way to do this? It feels like it could be hugely improved.
Current directory structure
The static class, I use this to get my classes (providers, I call them)
<?php declare(strict_types = 1);
namespace App;
class App {
private static $providers = array();
public static function getProvider($provider) {
if (!isset(self::$providers[$provider])) {
$providerName = 'App\Providers\\' . $provider;
self::$providers[$provider] = new $providerName();
}
return self::$providers[$provider];
}
}
Example of how I use it, I do this in all my controllers to show a view.
<?php declare(strict_types = 1);
use App\App;
namespace App\Controllers\Frontend\Guest;
class LoginController
{
public function getView()
{
\App\App::getProvider('Template')->renderTemplate('index.html');
}
}
Here is one of my few providers.
<?php declare(strict_types = 1);
namespace App\Providers;
class Template {
private $twig;
public function __construct() {
$this->twig = new \Twig_Environment(new \Twig_Loader_Filesystem(ROOT . '/resources/templates'), array(
'cache' => ROOT . '/storage/cache/templates',
));
}
public function renderTemplate($template) {
echo $this->twig->render($template, array('the' => 'variables', 'go' => 'here'));
}
}
Thanks in advance.
Static, singleton, global
I am sorry to disappoint you, because I am sure you invested a lot of effort in your code,... but you could or should take in consideration to start by not using any static methods (nor singletons or global variables). Here and here are some reasons presented.
Dependency injection (DI)
Inside a class, don't use methods of another class to create instances. If you need an object of any type, inject it - in constructor, or in any other method that needs it. This is called dependency injection. See this and this.
So, you should have this (further below I will change it):
Dependency injection container (DIC)
The structure that has the responsibility of injecting dependencies (e.g. wiring) is called dependency injection container (DIC). Examples: PHP-DI, or Auryn). It is constructed and used only at the entry point of your app (like in index.php or bootstrap.php). In it you will register your app dependencies, and it will automatically take care of injecting them all over. Your App class would then be redundant. And your application will "concentrate" itself on its actual purpose, not on the creation and/or implementation of the structures that it requires in order to be able to complete its tasks. The related term is Inversion of Control (IoC).
Service locator
As a note: don't pass the DIC as dependency. If you do that you are "creating" a so called service locator. It would mean that your class would depend on the container to fetch ("locate") some of its dependencies, or to run some of its processes through it.
Tight coupling
In analogy to the previous point: don't create objects using the
new
keyword inside a class, because you are making the class dependent of another - tight coupling. See this. The solution is the same as above: the dependency injection.Btw, in your code you are coupling each controller to the App class as well. As mentioned, this should be avoided.
So, in the Template class you should simply have:
and let the creation of the Twig environment to be performed in some other place in your code (e.g. by the DIC). Note the return statement, instead of echo: let the output of the rendered content happen on a higher level, e.g. at the bootstrap level (like index.php, or bootstrap.php) - see last code below.
Library for the main classes
Having said that, you could have a folder (like library or framework) on the project root niveau, in which all core classses such as the following could reside: Template, View, model layer constructs (like data mappers, repositories, db adapters, services, etc), Config, Session, HTTP related classes (Request, Response, Stream, Uri, etc). See this and this.
Routing
I see a routes.php file in your structure. So I am assuming that you are already familiar with the fact that the "translation" (parsing) of an URI - passed into the client (a browser) - into a route object containing the informations about a specific controller action (including the action parameters) is the responsibility of a router (most likely composed of more classes: Route, Dispatcher, Router, etc). Example of implementations: FastRoute, Aura.Router.
Front controller (request dispatching)
The task of dispaching the request, e.g. of finding - and, eventually, creating - a route object in a predefined routes collection (based on the provided URI) and calling the controller action (with the action parameters as arguments), is of the so called front controller. It will receive the router and a dispatcher as dependencies. In short, this front controller grabs a route object from the router and passes its informations to the dispatcher, which is responsible for calling the action. An example:
A good tutorial on this theme is presented in here, and the continuation here.
HTTP message abstraction (PSR-7)
Having in mind the nature of the web applications, based on request and response, you should familiarize yourself a bit with the PSR-7 recommendation. It provides an abstraction of the HTTP message - composed of a HTTP request and a HTTP response. A good standalone library is Zend-Diactoros.
Namespaces vs. file system structure (PSR-4)
Regarding the file system/namespaces structure: As per PSR-4 recommendation, a file with a class in it should have the same name as the class. Also, if you use imports (
use
keyword), then you don't have to use the fully qualified class name in other places of the code too. So, correct is like:Controllers and views - 1:1 relation
Please note that a view-controller relation could be 1:1. Their action methods would be then called separately (in the front controller):
In this constellation both, controller and view, can share different objects - like services, or domain objects (by many called "models"), or data mappers, etc. Let's say, that a controller and a view share a domain object - for example a "User" object. That means that both receive a certain domain object as constructor argument. This object will be, of course, automatically injected by the DIC.
First separation of concerns
By doing so, a first separation of concerns takes place:
Like:
Second separation of concerns - the MVC goal
In order to further separate the tasks, use data mappers, as intermediaries between the domain objects and the db (or the persistence layers, in general). E.g. in order to transfer data between the domain objects and the db.
In this step, the business logic, represented in and by the domain objects, become completely separated from any other app components. With this step, the actual goal of the MVC pattern is achieved: the decoupling of business logic from any other app structures/processes.
This can be seen in the following example: the User entity contains now only its properties and the methods related to them. The db functionality belongs now to the data mapper.
Further separations/optimizations
This great answer describes this part in detail.
And as last thing: all these dependencies in the examples are managed by the dependency injection container.
How to wire all together (steps):
• Define all "config" definitions, which will be registered into the dic. I speak about configuration values only, which will be further read by other "general" definitions of the dic, in order to create objects. The "config" definitions will probably reside in configuration files (e.g. definition files). So, for example, the definition file for the twig components could look like this:
config/configs/twig.php:
• Define all "general" definitions, which will be registered into the dic and are used to create objects. But I don't speak here about the "specific" definitions, used to create specific objects like controllers, views, domain ojects, data mappers, etc. The "general" definitions will probably reside in one file. For example:
config/generals.php:
• Define all "specific" definitions, which will be registered into the dic and are used to create objects. I speak here about the definitions used to create controllers, views, domain ojects, data mappers, etc, but only for the dependencies type-hinted with interfaces or not type-hinted at all ("skalar" parameters). These "specific" definitions will probably reside in one file:
config/specifics.php:
These "specific" definitions are based on the following LoginController:
and on the following LoginView:
Important: The Template dependency of the LoginView must not be defined in the "config/specifics.php" file, because for all concrete type-hints the DIC automatically creates instances! In this so-called autowiring process lies the real power of any DIC.
Note that
get
&object
in "config/specifics.php" are functions of the DIC.• Define all routes, which will be registered into the router. They could reside in config/routes.php
• In index.php only include the bootstrap file. So, index.php contains only one line:
From here on the work is made in bootstrap.php, which resides directly in the project root.
• Load Composer autoloader.
• Create an instance of the DIC builder.
• Register all previously defined definitions into the DIC.
• Compile the container, e.g. the DIC.
• Get the router from the DIC.
• Register all previously defined routes into the router.
• Read the uri from the browser.
• Pass it to the router and call the
dispatch
method of the router. The router compares the uri with each registered route and, if it finds a match, it will return an array with the informations contained in the registered route ("route infos" array).• The route infos are: the controller name, the action name and the action parameters list.
• Get the request object from the DIC by reading the 'request' entry from it and get the response object from the DIC by reading the 'response' entry from it. The generated response object will be probably used along the way before the controller action is called. For example, if you want to create a controller instance, but no controller class exists, then you'll use the response object to output an error message or a customized error page.
• Save the route infos into the request object as "attributes", e.g. with the withAttribute() method of the PSR-7 recommendation. From hear on read them only from the request object.
• Register the request object into the DIC by setting an entry
ServerRequestInterface::class
into it with the value of the request object. This way, because of autowiring, each time a dependencyServerRequestInterface
is needed everywhere the assigned request object containing the route infos will be automatically injected.• If you don't use the response object for some operations before calling controller/view action anymore, then you can already register it into the DIC by setting an entry
ResponseInterface::class
. This way, because of autowiring, each time a dependencyResponseInterface
is needed everywhere the assigned response will be automatically injected.• If an object of a certain type don't need to perform operations before the controller/view action is called, then you can register it as entry of DIC from the beginning. Otherwise, similar with request/response objects, create an instance of that type, use it, then register it afterwords. For example, a session object is needed to perform some session functions. After these ops are finished, a 'Session::class` entry can be registered in DIC.
• Based on the route infos and on the paths/namespaces registered as configs (e.g. "config" definitions in DIC) build the fully qualified class name (FQN) of the controller and of the view.
• Call the controller action. If not, no problem: the view action of the view is called. Make also proper validations (file/class exist, method exists). But use the
call()
method of the container (e.g. DIC), so that the automatic injection takes place!• Here is the most awaited moment: create a Template object (by using the configs/paths/namespaces registered in DIC inclusive the 'twigEnvironment' entry) and register it into DIC. For example:
In config/configs/app.php:
And now in bootsptrap.php:
• Call the view action if exists. If not, no problem: the 'output' method of the view is called. Make also proper validations (file/class exist, method exists). And use the
call()
method of the container (e.g. DIC), so that the automatic injection takes place!• Call the 'output' method of the view and print the result with
echo
. If none exists then throw an exception: the program is broken. Make also all proper validations (file/class exist, method exists). And use thecall()
method of the container (e.g. DIC), so that the automatic injection takes place!• The end...
Good luck.