Understanding key Object Oriented Programming concepts is required to be able to produce Modern, High Quality PHP Software Applications that stick to OOP Best Practices and Principles.
Prerequisites:
As I mentioned in past Article, Objects are instances of a Class that act as a template and provides all the properties and methods for the object to use.
Objects are very useful for encapsulating, organize and reuse code. When building a modern Enterprise Class PHP Software Application, leveraging the power of OOP is key.
When you start a new Application, a new Module or a new Feature, your code must respond to Client Requests or to Events happening. Clients can be either Users, or other Applications. Events can be things like a cron job set to run daily at 11 pm.
To facilitate the implementation process, there are OOP Software Design Patterns1 that are accepted as Standard models to follow in the production of new Code.
Basically, the most important things to keep in mind when developing new code are:
- Keep things well organized.
- Always use one file for one class.
- Use Dependency Injection instead of Class Inheritance as much as possible.
- Group Classes in folders based on the main type or functionality.
- When developing in a PHP Framework like Symfony, from the entry point of a Request where you start coding, to the exit point with a Response, do not make your code to jump from one method to another more than 3 ~ 5 times, and do not make these jumps to be in more than 3~4 files.
- Always use type casting and doc blocks with annotations. This will will help in building a very robust Enterprise Grade Application and also will help your IDE Intelli-Sense to better index and understand you code, facilitating finding and auto-completion.
- Implement your code in such way that each function does only one thing. It is also a must if you are required to implement PHP Unit Testing or building a TDD Application.
1. Keeping things well organized is very important for high quality code. The best way to achieve this is to follow industry proven patterns. If you are building Mini Services2 in REST API mode my recommendation for folder Structure is:
There is a main ApiModule that will have all class files that are used by the other Modules, are being used by the System through the Request-Response cycle and other particular actions-events that are not part of any of the other Modules.
All other Modules like the ClientsModule shown above need no more than these folders.
If you plan to use Trait classes, then you may want to put the folder in the ApiModule.
Follow the Mini Services Architecture Part 1 section for more on this.
2. The times where we put thousands of lines of code in one single file are long gone. Modern PHP development requires to have a File named the same as its Class, so autoloading and namespaces can be handled in a very efficient way by composer.
Also it is very important to have the code well organized among all different Class files in a project. This is a sample Entity Class for the Zoo project i used in other Articles:
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* Zoo
*
* @ORM\Table(name="zoo")
* @ORM\Entity
*/
class Zoo
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string|null
*
* @ORM\Column(name="zoo_name", type="string", length=45, nullable=true)
*/
private $zooName;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Animals", mappedBy="zoo", cascade={"persist", "refresh", "remove"}, orphanRemoval=true)
*/
private $animals;
public function __construct()
{
$this->animals = new ArrayCollection();
}
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return string|null
*/
public function getZooName(): ?string
{
return $this->zooName;
}
/**
* @param string|null $zooName
* @return $this
*/
public function setZooName(?string $zooName): self
{
$this->zooName = $zooName;
return $this;
}
/**
* @return Collection<int, Animals>
*/
public function getAnimals(): Collection
{
return $this->animals;
}
/**
* @param Animals $animal
* @return $this
*/
public function addAnimal(Animals $animal): self
{
if (!$this->animals->contains($animal)) {
$this->animals->add($animal);
$animal->setZoo($this);
}
return $this;
}
/**
* @param Animals $animal
* @return $this
*/
public function removeAnimal(Animals $animal): self
{
if ($this->animals->removeElement($animal)) {
// set the owning side to null (unless already changed)
if ($animal->getZoo() === $this) {
$animal->setZoo(null);
}
}
return $this;
}
}
And the file:
As mentioned in 1. keeping thing well organized is key. In this case Zoo Class is an Entity Class so it lives in the Entity Folder.
3. Class inheritance is one of the main characteristics of OOP. However, in modern development we have Dependency Injection available and if working with Symfony framework, DI works like magic when auto-wiring and auto-configure are enabled:
/**
* UsersService constructor.
* @param EntityManagerInterface $em
* @param CustomValidationServiceProvider $customValidation
* @param ApiResponseTransformer $apiResponse
* @param ValidatorInterface $validator
* @param UserPasswordEncoderInterface $passwordEncoder
* @param ContainerBagInterface $params
* @param ApiGuzzleClient $client
* @param DomainRolesService $domainRolesService
* @param PiiCryptoService $piiCryptoService
* @param Environment $twig
* @param AuthorizationManager $authorizationManager
*/
public function __construct(
EntityManagerInterface $em,
CustomValidationServiceProvider $customValidation,
ApiResponseTransformer $apiResponse,
ValidatorInterface $validator,
UserPasswordEncoderInterface $passwordEncoder,
ContainerBagInterface $params,
ApiGuzzleClient $client,
DomainRolesService $domainRolesService,
PiiCryptoService $piiCryptoService,
Environment $twig,
AuthorizationManager $authorizationManager
)
{
$this->em = $em;
$this->validator = $validator;
$this->customValidation = $customValidation;
$this->response = $apiResponse;
$this->passwordEncoder = $passwordEncoder;
$this->params = $params;
$this->httpClient = $client;
$this->domainRolesService = $domainRolesService;
$this->piiCryptoService = $piiCryptoService;
$this->twig = $twig;
$this->authorizationManager = $authorizationManager;
}
In this case, the above code is part of a Users Service Class that is in charge of all Business Logic related to Users. Each one the methods in each one of these Dependencies are available to be used just by doing something like this:
//Execute a Curl Post request using the httpClient
$response = $this->httpClient->getClient()->request('POST', $url, ['headers' => $headers, 'form_params' => $formParams]);
//Use the PII Crypto Service to hash the User Email
//Then use Entity Manager to query for this User in the DB
$user = $this->em->getRepository(User::class)->findOneBy(['email' => $this->piiCryptoService->hashData(base64_decode($email))]);
Also, these Dependencies can be injected in many other Classes. With this we follow PHP Best Practices and Stick to SOLID Principles.
Avoid as much as possible implementations like this:
<?php
//c.php
Class C
{
private $someCProperty;
public function __construct($params)
{
$this->someCProperty = $params->someCProperty;
}
public function car($c)
{
return 'this car';
}
}
//b.php
Class B extends C
{
public function __construct($params)
{
$paramsForC = $this->getParamsForCconstructor($params);
parent::__construct($paramsForC);
}
private function getParamsForCconstructor($params)
{
return $params->someOtherPropertiesForC;
}
public function cars($b, $c)
{
return parent::car($c) . 'cars from b' . $b;
}
}
//a.php
Class A extends B
{
private $someProperty;
public function __construct($params)
{
$paramsForB = $this->getParamsForBconstructor($params);
parent::__construct($paramsForB);
$this->someProperty = $params->someProperty;
}
private function getParamsForBconstructor($params)
{
return $params->someOtherPropertiesForB;
}
public function AvailableCars($a, $b, $c)
{
return parent::cars($b, $c);
}
}
//some-other-file.php
$carsFromA = new A($formParams);
$availableCars = $carsFromA->AvailableCars($a, $b, $c);
$carsFromBandC = new B($paramsForBnC);
$availableCarsFromBnC = $carsFromBandC->cars($b, $c);
Such implementations are leveraging the power of Class Inheritance, but doing this, you will end with a very messy code, very complicated to understand and very susceptible to fail every time an update or a new feature is added.
In the next Article, I will keep explaining the next recommendations.
1 For more info on this refer to:
2 Refer to my Articles here: