Tips of Building Large-scale Applications in PHP
When coding an application in PHP, for the sake of saving time or meeting our deadlines, we may often write functions that simply solve the specific problem we have at hand, without considering if the solution can be made general to solve other problems too: We focus on the tree and not in the forest.
Then, when a similar problem arises, we may copy/paste the previous solution, thus linearly increasing the lines of code in the project with respect to the number of problems needed to solve.
Repeat this process for some time, and the project becomes heavily bloated, starts getting hit with performance issues, new team members cannot understand how the logic works, and bugs are introduced with minimum effort. Certainly a situation to be avoided.
But it doesn’t need be so. The PHP language offers several features from the Object-Oriented Programming (OOP) paradigm that allows to effectively manage the complexity of our code, making it possible to have a lean application, the size of which increases more slowly than the number of problems to solve.
In this article we will make a tour of the most salient OOP features in PHP and demonstrate how they can be used.
Establishing a strong foundation through SOLID
Applying the SOLID principles allow to develop software that is to maintain in the long term, and easily customizable.
The SOLID principles are:
Principle | Description | |
---|---|---|
S | Single-responsibility principle | A class should have only a single responsibility |
O | Open-closed principle | Software entities should be open for extension, but closed for modification |
L | Liskov substitution principle | Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program |
I | Interface segregation principle | Many client-specific interfaces are better than one general-purpose interface |
D | Dependency Inversion Principle | One should depend upon abstractions, not concretions |
A wonderful explanation on how to implement the SOLID principles can be found in this article.
Solving common problems through Design Patterns
Design patterns are an invaluable tool for solving problems in a standardized way: These are descriptions of general, reusable solutions to several commonly occurring problems which happen when designing applications. Implementations of design patterns exist for every language, and several design patterns implemented in PHP can be found in this guide.
Decoupling the layers of the application through the Model-View-Controller paradigm
Model-View-Controller (MVC) is an architectural paradigm, which decouples the interacting parts of an application into three definite layers:
- The model manages the data, logic and rules of the application.
- The view renders the information.
- The controller is the link between the model and the view: it accepts inputs, requests data to the model, and passes the data to the view for rendering.
MVC is a great paradigm for building large-scale applications, because its separation of the frontend and the backend layers enables to assign the most appropriate team member to handle each part of the application: the Model and Controller components can be handled by backend developers and database architects, while the View component can be handled by frontend developers and designers. We can find several examples of MVC implementations for PHP online, from which I recommend this one.
The view layer can benefit from using a template engine such as Symfony’s Twig or Laravel’s Blade. Both Twig and Blade templates are eventually compiled to PHP code, so there is no overhead in terms of speed, yet they offer a concise syntax and a variety of additional features over plain PHP templates, which helps give clarity to the code, making it easier to understand and more maintainable.
Object inheritance
Classes and objects define the foundation of Object-Oriented Programming. Classes and their instantiation into objects enable the creation of highly-customizable components, which implement a specific functionality and can interact with other components. Code reusability is accomplished by manipulating the values of the class’ encapsulated properties, and through inheritance we can override or extend their behaviour. Modelling the application through classes makes it cheaper to test and maintain, since specific features can be decoupled from the application and managed on their own, and boost productivity, since using already-written components avoids reinventing the wheel every time, and the developer can better focus on coding the particular requirements of the application.
A class must declare the visibility of each property and function as either public
, protected
or private
, meaning that it is accessible from everywhere, from within the defining class and its ancestor and inheriting classes, or from within the defining class only, respectively. From within a function, the class’ properties are accessed through $this->
:
class Book {
protected $title, $author;
public function __construct($title, $author) {
$this->title = $title;
$this->author = $author;
}
public function getDescription() {
return sprintf(
__('The book\'s title is "%s" and its author is "%s"'),
$this->title,
$this->author
);
}
}
The new
keyword instantiates a class into an object, and the object’s properties and functions are accessed through ->
:
$book = new Book('Sapiens', 'Yuval Noah Harari');
echo $book->getDescription();
// This prints `The book's title is "Sapiens" and its author is "Yuval Noah Harari"`
A class can be inherited by a subclass, which can override the public
and protected
functions and access the value of the ancestor functions through parent::
:
class EBook extends Book {
protected $format;
public function __construct($title, $author, $format) {
parent::__construct($title, $author);
$this->format = $format;
}
public function getDescription() {
return sprintf(
__('%s, and its format is "%s"'),
parent::getDescription(),
$this->format
);
}
}
$ebook = new EBook('Sapiens', 'Yuval Noah Harari', 'epub');
echo $ebook->getDescription();
// This prints `The book's title is "Sapiens" and its author is "Yuval Noah Harari", and its format is "epub"`
The abstract
keyword defines a method that must be implemented by an inheriting class, and the class containing the abstract
method must itself be made abstract
, signifying that it cannot instantiated. The instantiation can be done only starting from the inheriting class which implements the abstract method:
abstract class Book {
abstract public function getTitle();
public function getDescription() {
return sprintf(
__('The book\'s title is "%s"'),
$this->getTitle()
);
}
}
// Book cannot be instantiated
class Sapiens extends Book {
public function getTitle() {
return 'Sapiens';
}
}
// Sapiens can be instantiated
$sapiens = new Sapiens();
The keyword static
defines methods and properties which live not under the object instantiating the class, but under the class itself, and these can be accessed through self::
from within the class, and the name of the class + ::
from outside it:
class Utils {
public static function print($html, $options) {
if ($options['pretty']) {
$html = prettify($html);
}
return $html;
}
public static function prettyPrint($html) {
return self::print($html, ['pretty' => true]);
}
}
echo Utils::prettyPrint('<div class="wrapper"><p>Hello <strong>world</strong>!</p></div>');
Reusing code through Traits
Classes that do not live under the same class hierarchy (i.e. who do not extend from one another) cannot reuse code through class inheritance. Traits is a mechanism that fills this gap, making it possible to reuse code among otherwise-unrelated classes. A trait is like a class in that it can define properties and functions, however, unlike a class, it cannot be instantiated. Instead, the code living inside a trait is exported into the composing class on compilation time.
The trait
keyword defines a trait, and the use
keyword imports a trait into a class. In the example below, classes Book
and CD
can reuse code through trait Sellable
:
trait Sellable {
protected $price;
public function getPrice() {
return $this->price;
}
public function setPrice($price) {
$this->price = $price;
}
}
class Book {
use Sellable;
}
class CD {
use Sellable;
}
$book = new Book('Sapiens', 'Yuval Noah Harari');
$book->setPrice(25);
A class can import code from more than one trait:
trait Printable {
public class print() {
// ...
}
}
class Book {
use Sellable, Printable;
}
Traits have several other features, such as enabling the composition of other traits, defining abstract methods, and offering a mechanism for conflict resolution whenever different traits sharing the same function name are composed under the same class.
Defining contracts through Interfaces
Interfaces enable to decouple the implementation of a functionality from declaring the intent of what the functionality must do. In essence, when using a functionality, we may not need to know how this is accomplished. As such, interfaces help define contracts among components, which are the basis for establishing the modularity of the application, enabling the application to be simply composed of components interacting with each other, without them caring how the other components do their tasks, and easily replaceable if/whenever needed.
The interface
keyword declares an interface, after which it lists the signatures of its methods:
interface EmailSender {
function sendAsHTML($to, $subject, $contents);
function sendAsPlain($to, $subject, $contents);
}
Making code manageable through Namespaces
Namespaces provides the same functionality to classes, traits and interfaces as directories provide to files in the operating system: they avoid conflicts whenever any two different elements have the same name by placing them on a different structure. Then, as much as two files can be called “foo.txt” by placing them on different directories, two classes can be called “Logger” by placing them under different namespaces.
The relevance of namespaces becomes apparent when interacting with several 3rd party libraries, since these may simultaneously use standard names such as “Reader” or “Writer” to name their elements, which would lead to conflict. Moreover, a single project can benefit from namespaces too to avoid classnames from becoming too long, such as “MyCompany_MyProject_Model_Author”.
The keyword namespace
declares a namespace, and it must be placed right on the line after the opening <?php
. A namespace can include several subnamespaces, separating them through \
:
<?php
namespace MyCompanyName\PackageName\Model;
class Author {
}
We can import the class into the current context and reference the class by its name directly:
use MyCompanyName\PackageName\Model\Author;
$author = new Author();
Alternatively, we can reference the class directly through its namespace + classname, trailing it with \
:
$author = new \MyCompanyName\PackageName\Model\Author();
Conclusion
Primarily since the release of version 7.0, PHP has become an extremely powerful language for building applications for the web, very versatile and resilient. In this article we reviewed its most salient features for building applications in a modular way, and several strategies for making the most out of Object-Oriented Programming, which can satisfy our goal of building large-scale applications which are easy to customize and maintain in the long-term.
Leave a Reply