PHP started out as a simple scripting language for generating HTML documents. As an open source project with many different contributors, it developed into the world’s most popular language for web applications. This development also led to its many shortcomings and inconsistencies. In the beginning, there were no uniform coding standards or best practices. Every developer stayed in their own lane.
Clear the stage
More and more, PHP developed into a language to be taken seriously, adapting various aspects of other programming languages. As applications became more professional, the need for standardization within the developer community grew. In the mid-2000s, many frameworks sprouted up to address these issues. Most frameworks from this time are still with us today, such as Symfony, Yii, CakePHP, and the Zend Framework (now renamed Laminas). Laravel joined relatively late in 2011, but it quickly became top dog. Because of their presence in the PHP world, we no longer question the use of frameworks. If a new project is lined up, the choice of framework is discussed, or the skeleton generator is started up. In the following, we will explain why this isn’t always a good idea and what alternatives there are.
IPC NEWSLETTER
All news about PHP and web development
Where frameworks score points
Let’s first take a closer look at the advantages of using frameworks. Programming beginners in particular can get started quickly thanks to a framework’s documentation, fixed structures, and rules. Developers who already have experience with other frameworks can usually familiarize themselves quickly. The framework community is supported by literature and tutorials. Like-minded people meet at conferences and get further support from online forums or external consulting. A few popular frameworks even offer paid services to help make software development even easier. Security vulnerabilities in web applications have given PHP a bad reputation. Often, errors were not due to PHP itself; frameworks were necessary to compensate for programming negligence (or ignorance). Security features such as escaping user input were available without a lot of programming effort, achieving wide acceptance. Components contained in frameworks meet high-quality standards and are well-coordinated. Many features can be activated with just the appropriate configuration files. This, along with a large number of available helper functions, leads to high development speed.
The downside
Since a framework has to be prepared for as many different use cases as possible, it cannot avoid considerable overhead. This leads to increased resource consumption or reduced execution speed compared to customized applications. Because of strong communities, more and more developers concentrate on their favorite frameworks. People often even call themselves Laravel or Symfony developers instead of PHP developers. But focusing on frameworks can lead to overlooking interesting developments happening outside of the framework ecosystem. One serious disadvantage is the update risk. Major updates usually come with breaking changes, which can cause high refactoring efforts. If the framework was not implemented correctly according to specifications (for instance, workarounds or bad practices have spread throughout code via copy and paste), then updating can be difficult or even impossible. This can endanger or even prevent long-term survival. It should not be underestimated, especially in commercial applications.
High development speed is also achieved by using antipatterns. For instance, Laravel Facades (box: “Antipattern: Laravel Facades”) binds code very closely to the framework [1]. This makes exchanging individual modules much more difficult. Unfortunately, this usually only becomes obvious after the project reaches a level of maturity or size, and the refactoring effort has increased disproportionately.
Antipattern: Laravel Facades
Laravel Facades are a good example of how wishing for simpler software development can lead to serious problems in the long run. Facades give developers direct access to the most important framework components anywhere in the code, without having to worry about dependency injection. Justifiably, some people may think of the (rather scorned) Singleton or Registry Pattern. A constructor with three or four parameters will be quickly exposed as a code smell. But if the dependencies can be integrated everywhere in the code on the fly, then uncontrolled growth is pre-programmed in the truest sense of the word. Problems arise especially if database schema are hardcoded while using the DB Facade Details. The following example from a controller is directly taken from the official Laravel documentation [2]:
$users = DB::select(‘select * from users where active = ?’, [1]);
return view(‘user.index’, [‘users’ => $users]);
In Laravel jargon, this syntax is expressive and elegant. You might be divided on that opinion. What happens if a column with the name deleted still has to be taken into account? That’s correct: all calls of this type must be found in the code and adapted. Even worse is having to replace the table with a microservice, which is not uncommon during later project phases. The users table is an illustrative example: If you took a little longer to write a user service at the beginning of the project, this wouldn’t be much of an issue. Eventually, only the logic in the service class itself would need to be adapted. Without this, a lot of manual work is required, because it rarely stays with simple queries.
Of course, using Laraval Facades (or similar approaches in other frameworks) is faster at first, making it very popular. Years later, after developers have left the company, their replacements become disillusioned when the aforementioned problems occur. It’s a prime example of technical debt [3].
YOU LOVE PHP?
Explore the PHP Core Track
Let’s go frameworkless!
Especially when it comes to popular architecture patterns such as microservices, where the focus is on applications that are as performant and simple as possible, you should ask yourself if there are alternatives. Look at the long-term disadvantages of using frameworks. But should we program everything by hand again, like in the past? Thankfully not, since there are many freely available components (usually in the form of Composter packages) that can do most of the work for us. However, from now on we have to write the manageable Glue Code that integrates and configures all of the packages. We will get help from PHP-FIG [4]. It originally began as a way to improve cooperation between frameworks. Over the years, it’s defined many basic standards, called PHP Standard Recommendations (PSR).
PHP-FIG is not without some controversy [5], but the resulting PSRs are now fixed components of the PHP ecosystem. With Composer, the initial PSR-0 (autoloading, in the meantime PSR-4) forms the cornerstone for most PHP applications and all modern frameworks. The PSRs cover important parts of the architecture spectrum. Additionally, at least one mature package is available for each PSR, as shown in Table 1.
Number | Description | Package or tool |
---|---|---|
PSR-3 | Logger Interface | monolog/monolog |
PSR-4 | Autoloading Standard | composer |
PSR-6 | Caching Interface | symfony/cache |
PSR-7 | HTTP Message Interface | laminas/diactoros |
PSR-11 | Container Interface | thephpleague/container |
PSR-1 and PSR-12 | Coding Standards | squizlabs/PHP_CodeSniffer |
PSR-14 | Event Dispatcher | thephpleague/event |
PSR-15 | HTTP Handlers | middlewares/awesome-psr15-middlewares |
PSR-17 | HTTP Factories | guzzle/psr7 |
PSR-18 | HTTP Client | guzzlehttp/guzzle |
Table 1: The most important PHP standard recommendations and package examples
One big advantage of using PSR-compliant packages is their interchangeability. If a package is no longer being developed or cannot be used for other reasons, it’s likely that another package can be used without having to refactor large parts of the application. Recommendations are based on established best practices, adding to the quality of our applications. Another advantage is that frameworks such as Laminas or Symfony are now basically component-based. The excellent modules can also be used when completely decoupled from the framework. We will make use of this in the following example. Although we don’t want to use a complete framework, there isn’t anything stopping us from using individual components.
A microservice without a framework
Let’s leave theory behind and have a look at a practical example: a simple microservice, which is certainly more common in this basic configuration. CRUD operations (Create, Read, Update and Delete) can be used to create, query, change, or delete data records. The actual range of functions is not important, as we are only interested in demonstrating the basic principle. There are many aspects among the PSRs that we need for our microservice: Autoloading, logging, DI Container, and everything about HTTP. Even a consistent codestyle is provided. Figure 1 shows the most important components of this service and which packages can be used as examples.
Fig. 1: A simple microservice without using a framework
Let’s look at the packages used here in detail:
- HTTP Request and Response: We entrust the idiosyncrasies of an HTTP request and its associated response to the laminas/laminas-diactoros component from the Laminas framework. It does outstanding work and is PSR-7 compliant.
- Router: This central component assigns the appropriate controller (not to be confused with the controllers from the MVC pattern) to the request, based on URL and the HTTP verb used (for example: GET). There is no separate PSR here; PSR-7 and PSR-15 provide a solid foundation. Thephpleague/route package is small, fast, and sufficiently flexible for our purposes.
- Authentication: Unless our web service will be used exclusively in a private network, then authentication is essential. PSR-15 compliant auth middleware can be found in middlewares/psr15-middlewares, which can be integrated in only a few steps. For our purposes, simple basic authentication is enough, but other concepts, like JWT Tokens, can also be implemented in the same way.
- Database: For our example, we are using a classic MySQL database. doctrine/dbal provides a simple, yet powerful abstraction layer. Of course, using another database, such as MongoDB, is also possible. PHP packages exist for every established database, but a PSR for databases does not exist (yet?).
- Logging: The PSR-3-compliant package monolog/monolog is almost a standard in its own right. It’s worthwhile to rely on monolog from the start because from logging, to local files, to cloud storage, an adapter is available for every case and is configured with just a few lines of code.
- Glue-Code: Even without a framework, we don’t want to forgo code quality and best practices. For dependency injection, we use the container thephpleague/container (PSR-11). Composer (PSR-4) is responsible for autoloading and as a coding standard, we choose PSR-12 or its predecessor PSR-2. squizlabs/PHP_CodeSniffer handles automatic checking.
- Tests: phpunit/phpunit is a solid choice for Unit Tests. But you can also use other testing tools like Atoum, Codeception, or phpSpec.
IPC NEWSLETTER
All news about PHP and web development
All components shown here are mature, regularly maintained, and have hundreds of thousands, or often millions of downloads. We do not use packages with a questionable future. In the worst case scenario, if a package is no longer maintained, it can be easily replaced thanks to standardization. The example in Listing 1 shows how easy it is to use packages of different origins together, thanks to the PSR interfaces. In contrast to the example above, here we use symfony/dependency-injection from the Symfony framework to demonstrate its flexibility. You would only have to adapt the section where the services and their dependencies are configured. The Symfony component works a bit differently here since the configuration of the DI container is not covered by PSR-11. Other than this, the component fits seamlessly and harmonizes with the router.
// Psr\Http\Message\ServerRequestInterface $request = Laminas\Diactoros\ServerRequestFactory::fromGlobals(); // Psr\Http\Message\ResponseFactoryInterface $responseFactory = new Laminas\Diactoros\ResponseFactory(); // Psr\Container\ContainerInterface $container = new Symfony\Component\DependencyInjection\Container(); // Configuration of services and their dependencies (shortened) $container->set(...); $strategy = new League\Route\Strategy\JsonStrategy($responseFactory); $strategy->setContainer($container); $router = new League\Route\Router(); $router->setStrategy($strategy); // Route Configurations (shortened) $router->map(...); $response = $router->dispatch($request);
More Code samples
You can find the accompanying sample microservice for this article in a GitHub repository [6]. There you will see the necessary glue code that pieces the above components together. The repository shows a few development stages of the service, organized in different branches. If you’re feeling brave, you can take a look at the branch of version 1, where the microservice was implemented solely using on-board PHP resources. Guaranteed to spark nostalgia or a spooky atmosphere.
A question of security
What about the security of frameworkless applications? Here, developers have an (even) greater responsibility, even if they use a lot of code from external packages that they cannot affect directly. From now on, we have to take care of package updates ourselves. But unlike with new framework releases, we won’t be notified about new updates through newsletters or similar things. Composer gives us composer outdated –direct, a necessary tool that can check which packages need to be updated. It’s recommended that you run the update check as a pre-commit Hook, or include it in your CI/CD Pipeline. While we’re at it, it’s also worth including the fabpot/local-php-security-checker package. It warns us about security vulnerabilities in unused packages. Regardless of whether or not a framework is used, the following is true: As soon as even just one line of custom code is written, the risk of security vulnerabilities increases. So, security measures such as penetration tests and security tools that automatically check code for potential gaps should be used. Other security measures can include team training in combination with mandatory code reviews.
Agnostic Code
Finally, let’s have a look at how we can make our code sturdier against outside interference. Frameworks and packages are constantly evolving – updates are simply unavoidable. But the framework agnostic approach can greatly limit the problem. In a nutshell, we should write our code so that we are directly using as few framework functions as possible. Business logic in particular can be easily incorporated into other applications, even if they are based on a different framework. Figure 2 shows the starting situation with a close link to the framework.
Fig. 2: A close link between the framework and your own code
Now, it’s a fallacy to assume that framework agnostic code will allow us to easily migrate an application to another framework. But, the approach gives us a big advantage: We organize and write our code in a way that minimizes dependencies on external components. Ideally, we wrap calls to classes and functions outside our scope in wrapper or service classes. Mind you, “outside our scope” also means the framework we are using, since we should never (really, never!) modify its code ourselves, no matter how tempting. Figure 3 shows how we can significantly reduce dependencies on the framework code by introducing a service class.
Fig. 3: Decoupling the framework code
Even completely replacing the DB component would only cause changes within this intermediate layer, and not in many individual places throughout the code. You should use this approach even without a framework, since we must also never (!) directly modify the code of any packages we use. For example: We use the SDK of an external web service to handle user authentication, which includes user rights. However, instead of calling the functions of this SDK directly, we write another service class. The SDK will communicate with the web service only within this class. If the web service is changed later, or if breaking changes are introduced in the SDK, only the service class has to be adapted. Therefore, you could also speak of package agnostic code. One good negative example of this is the HTTP client Guzzle. In a few years, several major releases were published that were not compatible with previous versions. Using a wrapper class to encapsulate the Guzzle API would have saved many projects a lot of work and frustration. Anyhow: Guzzle is now PSR-7 compliant.
Conclusion
The advantages of frameworks are undeniable, especially for start-ups, where innovation speed can often determine success or failure. But this is exactly where the issue lies: During the first few years, the application grows unchecked. Soon, programming patterns become ingrained and become incompatible with newer versions of the framework. That can only be fixed with a very high refactoring effort. Still, frameworks are a good choice when it comes to rapidly creating an MVP or a monolithic web application that needs many views and forms. However, code quality standards and patterns should be prescribed and monitored in the team at an early stage. Additionally, regular software maintenance (updates, eliminating code smells, etc.) should begin as early as possible, before technical debt becomes too large and hinders software development.
For experienced development teams, the frameworkless approach could be a welcome alternative to frameworks for long-lived services. Of course, in the beginning, this will involve some extra work. But once the basic framework is created, it can be a blueprint for future services and save development time. Make sure you have sufficient documentation and solid test coverage right from the start, then nothing can stand in the way of a robust PHP application.
Links & Literature
[1] https://programmingarehard.com/2014/01/11/stop-using-facades.html
[2] https://laravel.com/docs/8.x/database#running-a-select-query
[3] https://www.martinfowler.com/bliki/TechnicalDebt.html