Learn PHP Attributes with a Real World Usage example

In this tutorial we are going to learn PHP Attributes feature which was introduced with PHP 8.0 using a real world usage example. We are developing a simple web application using PHP attributes. At the end of this tutorial you will learn usage of PHP Attributes feature in a web application.

Task

Create a Post Request data validation example using PHP Attributes or simply validate DTO (Data Transfer object) using PHP Attributes with SOLID Principles.

Tutorial

Here we are developing a simple PHP application from scratch. Check end of this article to download full source code with postman files to test this application. Below image shows how we are going to implement this application including folder structure and file names.

Rules Folder
[ PHP Attribute classes ]
Rules Folder…
Validators Folder
[ actual validating
logic classes]
Validators Folder…
Required.php (PHP ATTRIBUTE class)
Required.php (PHP ATTRIBUTE cl…
RulesInterface.php
RulesInterface.php
ValidatorInterface.php
ValidatorInterface.php
isRequired.php
isRequired.php
getValidator(): validatorInterface
getValidator(): validatorInterface
Validator() function find declared attributes in the DTO class. This is where reflectionclass is used list defined PHP ATTRIBUTES
Validator() function find declared attributes in…
UserRegistrationDTO.php
UserRegistrationDTO.php
index.php
index.php
This file joins DTOs with attributes
This file joins DTOs with attri…
Validator.php
Validator.php
validator.validate(UserRegistrationDTO)
validator.validate(UserRegistrationDTO)
validate(DTO)
validate(DTO)
Has all available PHP ATTRIBUTES
Has all available…
get validator class from validators 
get validator class from v…
is Required value logic return true or false
is Required value logic return true…
PHP ATTRIBUTES are used in the properties of this class
PHP ATTRIBUTES are used in the properties of…
use both validator.php and UserRegistration.php. Call validotor() function in the validator.php 
use both validator.php and U…
Text is not SVG – cannot display

At first glace you will get confused thinking this a very complex application to learn PHP Attributes. Not at all. Above shows minimum requirements to create a good application with Object oriented concepts using SOLID principles. Take your time and read the above diagram.

Setup project

Skip this part of this tutorial if you already know how to create a simple PHP project from scratch. Okay, lets start the project. I am assuming you have already install PHP 8 or later version and Composer on your computer.

  1. Create project folder called “PHP_ATTRIBUTES”.
  2. Go to project folder and open your terminal or command prompt.
  3. Run below composer command.
composer init
  1. You will see vender folder, composer.json, composer,lock files. Open “composer.json” file. Modify psr-4 autoload settings as below.

I Hope there are learners who don’t know about this settings. This is used to set “src” folder as “App” for namespace. ex: namespace App\DTO

"autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
  1. Make sure to run composer dump-autoload command after modifying autoload settings.
composer dump-autoload
  1. Create “index.php” inside “src” folder. Add below code. Here we are accepting post requests made index.php file and all others requests are redirected to else part.
<?php
require_once "../vendor/autoload.php";

/**
* @method POST
* @URL "/"
* Accept only post request to root url 
*/
if($_SERVER['REQUEST_URI'] == '/' && $_SERVER['REQUEST_METHOD'] == "POST")
{
  //send json response
  header('Content-Type: application/json');
  echo "you have made a post request";
}else{
  echo "Welcome to PHP ATTRIBUTE example";
}
  1. Go to “src” folder and run below PHP command to create a local web server to test our application.
php -S localhost:8000

Open Postman application and check basic application setup by sending “post” and “get” requests to http://localhost:8000

Create DTO to hold post request data

DTO or data transfer objects are simple PHP classes with properties to hold data. We do not store business logic inside DTO classes. One and only purpose is to hold data. Let’s imagine we are receiving user registration post request with form data (username and email for this example). Let’s create UserRegistrationDto.

  • Create a folder called “DTO” inside the “src” folder.
  • Create “UserRegistrationDto.php” inside “DTO” folder.

Simply create a PHP class with two properties. Make sure to create readonly class. Because we do not want to mutate class properties

<?php
namespace App\DTO;

readonly class UserRegistrationDto{

  public function __construct(
    public string $username,   public string $email)
  {   }
}

Create PHP Attribute classes

Let’s create PHP attribute classes to used on UserRegistrationDto class properties which are $username and $email.

Let’s create a required validation for both $username and $email. This validation is used to make sure values in the UserRegistrationDto class are not empty. So let’s create a PHP Attribute class called “IsRequired“. In order to make a PHP Attribute use #[Attribute] on a normal PHP class. That’s all.

  • Create a folder inside DTO folder as “Validation“.
  • Create another folder inside “Validation” folder as “Rules“.
  • Create a file inside “Rules” folder as “IsRequired.php

Add below code to your “IsRequired” PHP Attribute Classs.

<?php
namespace App\DTO\Validation\Rules;

use Attribute;

#[Attribute]
class IsRequired{

  /* Return actual validator instance or class that 
    * has actual business logic to validate value
    */
  public function getValidator()
  {
  }
}

Why getValidator() function ?

Business logic isn’t implemented inside a PHP attribute class because attributes, or annotations, are meant to provide metadata about the code rather than control its behavior. Keeping the business logic in a separate class ensure clear code and maintainability. Therefore we use getValidator() function to return actual class that has the business logic to validate values.

Create Communication Class

We will create business logic to validate form data later. Before that we need a class to join both UserRegistrationDto class and IsRequired PHP Attribute class. Add created PHP Attribute class which is IsRequired validation to properties of UserRegistrationDto class.

  • Add #[IsRequired] attributes to class properties like below picture.
<?php
namespace App\DTO;

use App\DTO\Validation\Rules\IsRequired;

readonly class UserRegistrationDto{

  public function __construct(
    #[IsRequired]
    public string $username,

    #[IsRequired]
    public string $email)
  {   }
}

Let’s create a middleware class or communication class to join PHP attributes defined in UserRegistrationDto class with actual PHP attribute classes inside the “Rules” folder.

  • Create a file called “Validator.php” inside the “Validation” folder.
  • ReflectionClass is a php method used to extract properties, values that uses PHP Attributes.
  • Check comments provided on each method to understand why those methods are used.
<?php
namespace App\DTO\Validation;
use ReflectionClass;

class Validator{

  public function validate(object $dto):void
  {

    /**Here we are injecting concrete class instead of class name. 
     * Passing concrete class or already instantiated object to 
     * reflection class helps to extract not only property name 
     * but also property value. In our example it is UserRegistrationDto object
     */
    $reflector = new ReflectionClass($dto);

    //Get all available class properties from the passed object variable.
    //In this case dto
    foreach($reflector->getProperties() as $property)
    {

      //Extracting all the attributes
      $attributes = $property->getAttributes();
      
      /**
       * This returns a PHP Attribute class
       * If you are accessing methods or properties of 
       * that class make sure to instantiate it.
       */
      foreach($attributes as $attribute)
      {
         //to do
      }
    }
  }
}

Above code has a problems. Check the code line 23. It returns all defined PHP attributes. Here we need only the attributes related to validation only. Actually we do not use other attributes in this example, but it is a good practice to get only what you need.

getAttributes() method has a option to filter PHP Attributes based on php class. It is not a good practice to filter PHP Attributes based on a concrete class. If we use a concrete class we have to write filters for every validation class. Instead we are using a interface class.

Let’s filter only the PHP attribute classes related to validation that implements ValidationRuleInterface. For that we need to create a interface class and use on all the validation classes to make it filterable.

  • Create ValidationRuleInterface.php file inside the “Rules” folder.
<?php
namespace App\DTO\Validation\Rules;

interface ValidationRuleInterface{
  //return relavent validator class
  public function getValidator();
}

Now implement above interface class on IsRequired Attribute class.

......
#[Attribute]
class IsRequired implements ValidationRuleInterface{
 .......

Now we can filter attribute list to get only the attributes that implement above defined interface class. Modify getAttributes() method like below

use ReflectionAttribute;

......

// Extracting only the PHP Attributes that implements 
// ValidationRuleInterface
      $attributes = $property->getAttributes(
                                ValidationRuleInterface::class,
                                ReflectionAttribute::IS_INSTANCEOF
                              );

.....

Create actual business logic for validation

Now we have PHP Attribute class for “is Required” or not empty validation without the logic. Lets create the logic in a different file.

  • Create a folder called “Validators” to hold business logic for validation inside the “DTO” folder.
  • Create a file called “ValidatorInterface.php“.
<?php
namespace App\DTO\Validation\Validators;

interface ValidatorInterface{

  public function validate($value);

}

Create “IsRequiredRuleValidator.php” inside the “Validators” folder.

<?php
namespace App\DTO\Validation\Validators;

/**
 * Validate or implement logic for the rule attribute class on rules folder
 */
class IsRequiredRuleValidator implements ValidatorInterface{

  public function validate($value):bool
  {
    return !empty($value);
  }

}

Now complete “validate() method of the IsRequired PHP Attribute class to return object of the actual business logic class to validate.

<?php
namespace App\DTO\Validation\Rules;

use App\DTO\Validation\Validators\IsRequiredRuleValidator;
use App\DTO\Validation\Validators\ValidatorInterface;
use Attribute;

#[Attribute]
class IsRequired implements ValidationRuleInterface{

  /**
   * Return relavent instance of validator available in validators folder
   * Return required class from validators
   */
  public function getValidator():ValidatorInterface
  {
    return new IsRequiredRuleValidator();
  }
}

Now we can complete “validate()” method of the validator class. In simply, this validate() method does below things.

  • Uses PHP attributes ReflectionClass to get all related PHP Attributes.
  • Then call getValidator() method to get actual class that holds the business logic to validate.
  • getValidator() method return already instantiated object (check line number 17 of IsRequired class). You can directly call validate() method of the class that hold actual business logic with property value to validate and return Boolean.

Open validator.php file and modify the validate() function. Below shows final version of the validate() function.

<?php
namespace App\DTO\Validation;

use App\DTO\Validation\Rules\ValidationRuleInterface;
use ReflectionAttribute;
use ReflectionClass;

class Validator{

  private $errors = [];
  public function validate(object $dto):void
  {

     /**Here we are injecting concret class instead of class name. 
     * Passing concret class or already instantiated object to 
     * reflection class helps to extract not only property but also
     * property values.
     */
    $reflector = new ReflectionClass($dto);

    //Get all available class properties from the passed object variable.
    //In this case dto
    foreach($reflector->getProperties() as $property)
    {

      //Filtering and extracting only the PHP Attributes that 
      // implements ValidationRuleInterface
      $attributes = $property->getAttributes(
                                ValidationRuleInterface::class,
                                ReflectionAttribute::IS_INSTANCEOF
                              );
      
      /**
       * This return a php class where you define as a #[Attribute]
       * If you are accessing methods or properties of 
       * that class make sure to instantiate it.
       */
      foreach($attributes as $attribute)
      {
        //Validator is a object method of validatorRuleinterface
        $validator = $attribute->newInstance()->getValidator();

        //If validation failed.
        if(!$validator->validate($property->getValue($dto)))
        {
          $attributeName = $attribute->getName();
          $parts = explode('\\', $attributeName);
          $errorName = end($parts);

          $this->errors[$property->getName()][] = sprintf(
                            "%s validation failed",
                            $errorName, 
                            );
        }

      }
    }
  }
}

Add another method to validator class to get errors.

..... 
/**Return errors */
  public function getErrors($index=''):array|string
  {
    if($index){
      $errorMessage = isset($this->errors[$index][0])?$this->errors[$index][0]:'';
      return $errorMessage;
    }else{
      return $this->errors;
    }
    
  }
....

Complete index.php

Lets complete index.php file.

  • Get form data from the post request.
  • Create DTO – UserRegistarionDto object with form data.
  • Create an instance of validator (which joins attributes defined in DTO and actual Attribute classes)
  • Pass DTO object to validate() method of the validator instance to validate.
<?php
require_once "../vendor/autoload.php";

use App\DTO\UserRegistrationDto;
use App\DTO\Validation\Validator;

/**Accept only post request to root url */
if($_SERVER['REQUEST_URI'] == '/' && $_SERVER['REQUEST_METHOD'] == "POST")
{
  //send json response
  header('Content-Type: application/json');
  
  //Get post request data
  $username = $_POST['username'] ?? '';
  $email = $_POST['email'] ?? '';

  //Create DTO
  $userRegistrationDto = new UserRegistrationDto($username, $email);

  //Validate DTO
  $validator = new Validator();
  $validator->validate($userRegistrationDto);
  
  //JSON encode DTO Errors
  $result = json_encode($validator->getErrors());
  echo $result;

}else{
  echo "Welcome to PHP ATTRIBUTE example";
}

Source Files

Download source files of the above example using below download link.

Download Source code

Above source code has all the composer dev requirements and postman files. No need to run composer install to install requirements.