popcorn4dinner/policies

easy to use, non-opinionated library to model complex rules for validations, access control, etc

0.1.2 2018-04-19 13:00 UTC

README

A PHP library to easily model complex rules or policies for validations, access control, etc

installation

composer require popcorn4dinner/policies 

Example: Validating a user

  1. define actions or events as an enum, here CREATE, REGISTER, UPDATE, DELETE and ADMIN_UPDATE
  2. create one policy for each of your business rules
  3. create a Validator to use your policies and define exceptions

creating policies

email address must have the right format

<?php

namespace Popcorn4dinner\Policies\Examples\Policies;

use Popcorn4dinner\Policies\AbstractPolicy;
use Popcorn4dinner\Policies\Examples\User;
use Popcorn4dinner\Policies\Examples\UserAction;
use Popcorn4dinner\Policies\Examples\UserRepositoryInterface;

class EmailFormatPolicy extends AbstractPolicy
{
    protected const ERROR_MESSAGE = 'Invalid email address';

    /**
     * @param MvpUser $user
     * @return bool
     * @throws PolicyValidationException
     */
    protected function isViolatedWith(MvpUser $user, UserAction $action): bool
    {
        return $this->isInvalidEmail($user->getEmail());
    }

    /**
     * @param string $email
     * @return bool
     *
     */
    private function isInvalidEmail(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) === false;
    }
}

email address must be unique

<?php

namespace Popcorn4dinner\Policies\Examples\Policies;

use Popcorn4dinner\Policies\AbstractPolicy;
use Popcorn4dinner\Policies\Examples\User;
use Popcorn4dinner\Policies\Examples\UserAction;
use Popcorn4dinner\Policies\Examples\UserRepositoryInterface;

class EmailUniquenessPolicy extends AbstractPolicy
{
    protected const ERROR_MESSAGE = 'User with email already exists.';

    /**
     * @var UserRepositoryInterface
     */
    private $userRepository;

    /**
     * EmailPresencePolicy constructor.
     * @param UserRepositoryInterface $userRepository
     */
    public function __construct(UserRepositoryInterface $userRepository, UserAction ...$excludedActions)
    {
        parent::__construct(...$excludedActions);
        $this->userRepository = $userRepository;
    }

    /**
     * @param MvpUser $user
     * @return bool
     * @throws PolicyValidationException
     */
    protected function isViolatedWith(MvpUser $user, UserAction $action): bool
    {
        return !$this->isUniqueEmail($user->getEmail());
    }

    private function isUniqueEmail(string $email): bool
    {
        return $this->userRepository->findByEmail($email) === null;
    }
}

password must be longer then 8 characters

<?php

namespace Popcorn4dinner\Policies\Examples\Policies;

use Popcorn4dinner\Policies\AbstractPolicy;
use Popcorn4dinner\Policies\Examples\User;
use Popcorn4dinner\Policies\Examples\UserAction;
use Popcorn4dinner\Policies\Examples\UserRepositoryInterface;

class PasswordLengthPolicy extends AbstractPolicy
{
    protected const ERROR_MESSAGE = 'Password not long enough.';

    private const MIN_PASSWORD_LENGTH = 8;

    /**
     * @param MvpUser $user
     * @return bool
     * @throws PolicyValidationException
     */
    protected function isViolatedWith(MvpUser $user, UserAction $action): bool
    {
        return $this->isInvalidPassword($user->getPassword());
    }

    private function isInvalidPassword(string $password): bool
    {
        return strlen($password) < static::MIN_PASSWORD_LENGTH;
    }
}

password must be secure enough

<?php

namespace Popcorn4dinner\Policies\Examples\Policies;

use Popcorn4dinner\Policies\AbstractPolicy;
use Popcorn4dinner\Policies\Examples\User;
use Popcorn4dinner\Policies\Examples\UserAction;
use Popcorn4dinner\Policies\Examples\UserRepositoryInterface;

class PasswordCharactersPolicy extends AbstractPolicy
{
protected const ERROR_MESSAGE = 'Password not strong enough.';

private const NUMBER_IN_PASSWORD = '#[0-9]+#';
private const LOWERCASE_CHARACTERS = '#[a-z]+#';
private const UPPERCASE_CHARACTERS = '#[A-Z]+#';

    /**
     * @param MvpUser $user
     * @return bool
     * @throws PolicyValidationException
     */
protected function isViolatedWith(MvpUser $user, UserAction $action): bool
{
    return $this->isInvalidPassword($user->getPassword());
    

    private function isInvalidPassword(string $password): bool
    {
        $includesNumber = preg_match(static::NUMBER_IN_PASSWORD, $password);
        $includesUpperCaseCharacters = preg_match(static::UPPERCASE_CHARACTERS, $password);
        $includesLowerCaseCharacters = preg_match(static::LOWERCASE_CHARACTERS, $password);

        return !($includesNumber && $includesUpperCaseCharacters && $includesLowerCaseCharacters);
    }
}

creating a validator

<?php

namespace Popcorn4dinner\Policies\Examples;

use Popcorn4dinner\Policies\Examples\Policies\EmailFormatPolicy;
use Popcorn4dinner\Policies\Examples\Policies\EmailUniquenessPolicy;
use Popcorn4dinner\Policies\Examples\Policies\PasswordLengthPolicy;
use Popcorn4dinner\Policies\Examples\Policies\PasswordCharactersPolicy;
use Popcorn4dinner\Policies\BasicValidator;

class UserValidatorFactory
{
    /**
     * @param UserRepositoryInterface $userRepository
     * @return UserValidator
     */
    public function create(UserRepositoryInterface $userRepository)
    {
        return new BasicValidator(
            new EmailFormatPolicy(),
            new EmailUniquenessPolicy($userRepository),
            new PasswordLengthPolicy(UserAction::ADMIN_UPDATE()),
            new PasswordStrengthPolicy(UserAction::ADMIN_UPDATE())
        );
    }
}

use PolicyCollections to model more complex rules

class UserValidatorFactory
{
    /**
     * @param UserRepositoryInterface $userRepository
     * @return UserValidator
     */
    public function create(UserRepositoryInterface $userRepository)
    {

        $userPolicies = new PolicyCollection(
            new PasswordLengthPolicy(UserAction::ADMIN_UPDATE());
            new PasswordStrengthPolicy(UserAction::ADMIN_UPDATE())
        )

        $universalPolicies = new PolicyCollection(
            new EmailFormatPolicy(),
            new EmailUniquenessPolicy($userRepository)
        )

        return new BasicValidator(
            $universalPolicies,
            $standardUserPolicies
        );
    }
}