The modern PHP approach to creating modular applications

How to build web apps in terms of reusable modules and without any additional custom code.

There are many ways of using PHP and many end goals for choosing one way over another. At our company, Proximify, our end goal is to use it for creating API-wrapped backend functionality entirely in terms of components that are reusable and swappable. The text below shows how we do it. More precisely, it shows the steps to follow in order to create components that can be added to a base skeleton app in other to build a complete, well-orchestrated solution for a backend application and its API.

For us, an app’s module is a bundle of services that has its own classes, module dependencies, configuration, assets, documentation and tests. In other words, it’s a plugin that can be installed with a package manager into a host app or used as a dependency in another module.

The following text is a step-by-step guide for creating an application exclusively in terms of private and public service bundles. The approach shown works for building both proprietary and open source applications.

TL;DR

You can create a backend web app entirely based on modules that are installed into a skeleton application. The wrapping functionality needed to plugin a module into an app is known as a bundle, and the appropriate modularity is achieved by creating components, which group classes into functional packages.

What is app modularity?

The concept of app modularity is often used intuitively and without proper definition. While that is usually enough, in our context it is worth noticing that app modularity appears at different levels of an app’s source code, so it’s truly a multi-scale pattern. In this section, we discuss the nature of modularity to understand the theory behind the practical implementation. You can skip this section altogether and head over the Components and Bundles section if you just want the how-to guide.

Most of us are familiar with at least some specific levels of modularity. The function level is the grouping of language statements into functions while the object level is the grouping of functions into classes. Additional scales of modularity exists both up and down those two levels. Because of that, stating that our goal is to build a modular app is not sufficiently specific unless we’ll also define the modularity levels that we target.

Modularity is not an abstraction but a pattern enabled by a set of concrete properties. For example, in the physical world there is an atomic level, a molecular level, a protein level, a cellular level and so on. Each level makes use of concrete features from lower levels and emerges from them. The molecular level exists because of the atomic forces that bind atoms together. The concept of “binding” is recurrent at all levels in one way or another. A modularity level arises as a result of the binding properties of lower levels.

Back to the software world, we are interested in the modularity within the domain of coding and right above the object level. That’s a very specific slice of the modularity stack. We are not concerning ourselves with how the app interoperates with other apps or micro-services, and we assume that the code is already modular up to the object level. Such scales in the modularity stack are collectively known as the module level, which is not particularly descriptive. Other names are: package, library, bundle, plugin, and addon. Leaving semantics aside, what’s most important is the binding properties that we can use to add higher levels of modularity to an app.

Most web apps today are built in terms of packages and custom code. The custom code adds domain logic tailored to the desired solution. In our view, that type of app as a hybrid between non-modular and modular. The packages installed into the app are indeed modules, but the rest of the code is not because it’s not grouped into modules. There is of course nothing wrong with writing code directly into an app, but we are interested in making purely modular apps. A modular app is essentially just a folder with configuration files, assets and modules.

Components and Bundles

We’ll consider two levels of modularity called component and bundle. Autoloading and dependency injection are the coding patterns that enable such modular constructions.

Components

A component is a grouping of object classes into a project folder with optional dependencies on other components. The binding properties that makes a component possible is the class autoloading functionality of the PHP language, which is enhanced by the package managent functionality provided by Composer.

Components usually place their source code at the project’s root folder or under a src subfolder and define their own tests under a tests subfolder. A typical component’s autoloader rule in a composer.json file is:

“psr-4”: { “{VENDOR}\\Component\\{NAME}\\”: “src” }

Bundles

A bundle is a grouping of classes, components and/or bundles that exposes selected classes as services for a host app. The host app is the app into which the bundle is installed. The subset of classes in the bundle that are set to be registered with the host app are called services. So the terms bundle can be understood as meaning bundle of services for an application.

The main difference between a bundle and a component is that a bundle registers some of its classes with a host app in order to make them part of the app’s dependency injection functionality whereas a component doesn’t. Auto-wiring and auto-configuration are common names given to different steps in the automation of the registration process into the host app. The binding property that makes a bundle possible is the dependency injection functionality of the host app.

While a bundle might be made out of one or more classes and depend on components and/or other bundles, a common use case is to make a bundle only out of components. For such cases, a bundle could be understood as a bundle of components. However, a bundle’s intent is always to wire services into the dependency injection of a host app. Otherwise, a bundle of components would just be the special case of a component that is made out of other components.

Components don’t have to comply with specific rules other than defining the autoload property in their composer.json. Bundles, however, are more complicated. We explain the steps needed to create a bundle in a companion article and also show how to test a bundle without a host app.

In summary, autoloading and dependency injection are two binding functionalities that enable additional levels of modularity in PHP apps.

The Auto-X Toolchain

Autoloading, auto-wiring, and auto-configuring are functionalities that define a toolchain for assembling a modular app. We call it the auto-x toolchain. The toolchain manages the installation of packages and the dependency injection (DI) of services provided by the packages. There are many different tooling options that can achieve that functionality. Our particular choice of toolchain is PHP, Composer, and Symfony DI.

PHP provides the much needed autoload functionality while Composer is in charge of downloading packages and making sure that the version constraints of all dependencies are satisfied. In turn, Symfony is used to inter-connect and configure services by performing what is known as auto-wiring and auto-configuring.

We will not be following the Symfony approach to building web apps because it leads to hybrid apps. Instead, we will only rely on Symfony’s DI mechanism and its conventions for service registration in order to end up with a modular app. We can then use Symfony components just like any other component since in our approach all the component in the app are external to it.

Autowiring enables the configuration of services in the service container of the host app by making use of metalanguage information. More specifically, the autowire logic reads the type-hints on constructor methods (or other methods) and automatically passes the correct services to each method.

A bundle is a set of classes and service declarations. Our job is to create projects (sub-repositories) that provide bundles to a generic app. Our auto-x approach rejects the writing of code directly into the app’s project. Hence, all functionality must be provided in terms of service bundles installed into the host app via a package manager.

A monorepo structure can be used to keep all packages and apps in one single git repo. Composer’s path repositories are a good solution to install local components and bundles into host apps within the same repo. In addition, some or all the components and/or bundles can be in their own git repositories and: (a) available from Packagist or Private Packagist; (b) self-hosted; or (c) fetched from a local or remote repo (e.g. GitHub).

Creating the app scaffold

Let’s create a generic app skeleton named “my-app”.

$ composer create-project symfony/skeleton my-app
$ cd my-app

The installation process will end with a recommendation by Symfony to “Download the Symfony CLI”, which is a good idea because it provides an easy way to launch a local web server via symfony server:start -d, see a live tail of the logs with symfony server:log, and stop the server with symfony server:stop.

Now that we have an app’s project, we can explicitly state that we’ll not be writing any code directly into the my-app project. We will achieve the desired backend functionality by adding service bundles and configuration files to the project and nothing more. The src folder of the backend project shall remain empty!

Next, let’s create a bundle by following the step-by-step instructions in the companion article. The results should be a simple ReportMaker bundle that produced a Word document with the text that it receives in the parameters of a request. The naming convention for Composer packages that are bundles is to add the suffix -bundle to the base name. That’s makes sense when the bundle is a wrapper of components and the base name is already in use by one of the components. In our simple example, the bundle is made out of classes, so we called it report-maker instead of report-maker-bundle. It’s up to you to decide if that choice is appropriate or not.

The bundle should include a controller class within its src/Controller subfolder in order to declare the routes that it handles (via method annotations or PHP attributes). The controller in the example declares the route /reports.

<?php
// src/Controller/Reports.php
namespace App\Controller;// ...class Reports
{
/**
* @Route("/reports", name="reports")
*/
function makeReport(Request $req, ReportMaker $maker): Response
{
// ...
}
}

Since the src/Controller folder is the standard location where the Symfony routing system looks for routes, we only have to declare that our bundle handles routes by adding a config/routes/report-maker.yaml configuration file in the host app’s project.

# config/routes/report-maker.yamlreport-maker:
resource: '@ReportMaker'
prefix: '/api'

The prefix for the route is optional. Here we added /api to show that we have control over what sub-routes a bundle gets to manage. In this case, the final route to get a Word document using the bundle becomes api/reports.

We can test that it all works well by starting a web server

symfony server:start

and then visiting the webpage

http://localhost:8000/api/report?text=We_are_done!

Conclusions

We have shown that it is possible to create a web application without writing code into it. Instead, we can achieve total modularity by adding bundles to the app, which in turn can be made out of components. The binding functionality that makes all of this possible is the dependency injection and the autoloading functionality.

The dependency injection functionality must be added into the app via some bundle. In our example, the symfony/dependency-injection component is included in the symfony/framework-bundle, which is required in the skeleton app that we used. The autoloading functionality is provided by PHP and by the path declarations loaded with the vendor/autoload.php created by the Composer package manager.

Where do we go from here?

We showed how to create a bundle but not how to create a component. Hopefully that’s not a problem because there is nothing special about a component other that it needs an autoload declaration in its composer.json file so that it’s classes are autoloaded. We showed the typical psr-4 declaration above.

In general, an app will also require third-party bundles. Common bundles are:

// Database abstraction
$ composer require doctrine/doctrine-bundle
// File system abstraction (including cloud storage)
$ composer require league/flysystem-bundle
// API functionality
$ composer require api-platform/api-pack

Now that we covered the the basics of app modularity, the question is what’s next. Probably, the natural follow up to this article is a discussion of modularity beyond source code. Cloud services enable new forms of modularity that are worth exploring such as micro-services and lambda functions. As we mentioned earlier, modularity is a multi-scale property that can emerge at different levels of a software solution as long as the needed binding functionality is present.

CEO/CTO @Proximify. Specialized in research information systems.