How to refactor Symfony 5 controller to comply with SOLID design principles

I am trying to create this API endpoint that will accept JSON payload and will calculate quote based on provided factors and their ratings.

I have Entities that contain information about

  • "age",
  • "postcode" and
  • "ABI code" ratings.

These AgeRating, PostcodeRating and AbiRating entities implement RatingFactorInterface to force implementation of getRatingFactor() method.

QuoteController seems to be violating "Single Responsibility" and "Open/Close" design principles as the factors like "age", "postcode" can change – extra factor can be added or one of the factors might not be used.

I was thinking maybe it would be possible for rating factors to be specified in the dependency injection container, but can’t seem find a good example how this would work especially with factors that depend on other services like AbiCodeRating which also depends on ABI code which is returned by using third party API which accepts car registration number.

How do I refactor the controller and services so I’m not violating Single Responsibility and Open / Close design principles?

POST JSON Payload example

{
    "age": 20,
    "postcode": "PE3 8AF",
    "regNo": "PJ63 LXR"
}

QuoteController

<?php

namespace AppController;

use AppRepositoryAbiCodeRatingRepository;
use AppRepositoryAgeRatingRepository;
use AppRepositoryPostcodeRatingRepository;
use AppServiceAbiCodeLookup;
use AppServiceQuoteCalculator;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentRoutingAnnotationRoute;

/**
 * Class QuoteController
 * @package AppController
 */
class QuoteController extends AbstractController
{
    /**
     * @Route("/", name="quote")
     *
     * @param Request $request
     * @param AbiCodeRatingRepository $abiCodeRatingRepository
     * @param AgeRatingRepository $ageRatingRepository
     * @param PostcodeRatingRepository $postcodeRatingRepository
     * @return JsonResponse
     */
    public function index(Request $request, AbiCodeRatingRepository $abiCodeRatingRepository, AgeRatingRepository $ageRatingRepository, PostcodeRatingRepository $postcodeRatingRepository)
    {
        try{

            $request = $this->transformJsonBody($request);

            /**
             * Quoting engine could be used with a different set of rating factors!
             * Meaning age, postcode and regNo maybe not be required, some other rating factors might be introduced
             * How to make controller to accept rating factors dynamically?
             */
            if (!$request || !$request->get('age') || !$request->request->get('postcode') || !$request->get('regNo')){
                throw new Exception();
            }

            /**
             * call to a third party API to look up the vehicle registration number and return an ABI code
             * this is only required if AbiRating is used with the quoting engine
             */
            $abiCode          = AbiCodeLookup::getAbiCode($request->get('regNo'));
            /**
             * $abiCode is only required if postcodeRating is used by quoting engine
             */
            $ratingFactors[]  = $abiCodeRatingRepository->findOneBy(["abiCode"=>$abiCode]);
            $ratingFactors[]  = $ageRatingRepository->findOneBy(["age"=>$request->get("age")]);
            /**
             * $area is only required if postcodeRating is used by quoting engine
             */
            $area             = substr($request->get("postcode"),0,3);
            $ratingFactors[]  = $postcodeRatingRepository->findByPostcodeArea($area);
            $premiumTotal     = QuoteCalculator::calculate($ratingFactors);

            $data = [
                'status' => 200,
                'success' => "Quote created successfully",
                'quote' => $premiumTotal
            ];

            return new JsonResponse($data,200);

        }catch (Exception $e){
            $data = [
                'status' => 422,
                'errors' => "Data is not valid",
            ];
            return new JsonResponse($data,422);
        }

    }

    /**
     * @param Request $request
     * @return Request
     */
    protected function transformJsonBody(Request $request)
    {
        $data = json_decode($request->getContent(), true);

        if ($data === null) {
            return $request;
        }

        $request->request->replace($data);

        return $request;
    }
}

AbiCodeRating

<?php

namespace AppEntity;

use ApiPlatformCoreAnnotationApiResource;
use AppRepositoryAbiCodeRatingRepository;
use DoctrineORMMapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={"get","post"},
 *     itemOperations={"get"}
 * )
 * @ORMEntity(repositoryClass=AbiCodeRatingRepository::class)
 */
class AbiCodeRating implements RatingFactorInterface
{
    /**
     * @ORMId
     * @ORMColumn(type="string", length=10)
     */
    private $abiCode;

    /**
     * @ORMColumn(type="decimal", precision=10, scale=2, nullable=true)
     */
    private $ratingFactor;


    public function getAbiCode(): ?string
    {
        return $this->abiCode;
    }

    public function setAbiCode(string $abiCode): self
    {
        $this->abiCode = $abiCode;

        return $this;
    }

    public function getRatingFactor(): ?float
    {
        return $this->ratingFactor;
    }

    public function setRatingFactor(?float $ratingFactor): self
    {
        $this->ratingFactor = $ratingFactor;

        return $this;
    }
}

AgeRating

<?php

namespace AppEntity;

use ApiPlatformCoreAnnotationApiResource;
use AppRepositoryAgeRatingRepository;
use DoctrineORMMapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={"get","post"},
 *     itemOperations={"get"}
 * )
 * @ORMEntity(repositoryClass=AgeRatingRepository::class)
 */
class AgeRating implements RatingFactorInterface
{
    /**
     * @ORMId
     * @ORMColumn(type="integer")
     */
    private $age;

    /**
     * @ORMColumn(type="decimal", precision=10, scale=2, nullable=true)
     */
    private $ratingFactor;


    public function getAge(): ?int
    {
        return $this->age;
    }

    public function setAge(int $age): self
    {
        $this->age = $age;

        return $this;
    }

    public function getRatingFactor(): ?float
    {
        return $this->ratingFactor;
    }

    public function setRatingFactor(?float $ratingFactor): self
    {
        $this->ratingFactor = $ratingFactor;

        return $this;
    }
}

PostcodeRating

<?php

namespace AppEntity;

use AppRepositoryPostcodeRatingRepository;
use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass=PostcodeRatingRepository::class)
 */
class PostcodeRating implements RatingFactorInterface
{
    /**
     * @ORMId
     * @ORMColumn(type="string", length=4)
     */
    private $postcodeArea;

    /**
     * @ORMColumn(type="decimal", precision=10, scale=2, nullable=true)
     */
    private $ratingFactor;

    public function getPostcodeArea(): ?string
    {
        return $this->postcodeArea;
    }

    public function setPostcodeArea(string $postcodeArea): self
    {
        $this->postcodeArea = $postcodeArea;

        return $this;
    }

    public function getRatingFactor(): ?float
    {
        return $this->ratingFactor;
    }

    public function setRatingFactor(?float $ratingFactor): self
    {
        $this->ratingFactor = $ratingFactor;

        return $this;
    }
}

RatingFactorInterface

<?php


namespace AppEntity;


interface RatingFactorInterface
{
    /**
     * @return float|null
     */
    public function getRatingFactor(): ?float;
}

AbiCodeRatingRepository

<?php

namespace AppRepository;

use AppEntityAbiCodeRating;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;

/**
 * @method AbiCodeRating|null find($id, $lockMode = null, $lockVersion = null)
 * @method AbiCodeRating|null findOneBy(array $criteria, array $orderBy = null)
 * @method AbiCodeRating[]    findAll()
 * @method AbiCodeRating[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class AbiCodeRatingRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, AbiCodeRating::class);
    }

   
}

AgeRatingRepository

<?php

namespace AppRepository;

use AppEntityAgeRating;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;

/**
 * @method AgeRating|null find($id, $lockMode = null, $lockVersion = null)
 * @method AgeRating|null findOneBy(array $criteria, array $orderBy = null)
 * @method AgeRating[]    findAll()
 * @method AgeRating[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class AgeRatingRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, AgeRating::class);
    }
}

PostcodeRatingRepository

<?php

namespace AppRepository;

use AppEntityPostcodeRating;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;

/**
 * @method PostcodeRating|null find($id, $lockMode = null, $lockVersion = null)
 * @method PostcodeRating|null findOneBy(array $criteria, array $orderBy = null)
 * @method PostcodeRating[]    findAll()
 * @method PostcodeRating[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class PostcodeRatingRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, PostcodeRating::class);
    }

    /**
     * @return PostcodeRating Returns PostcodeRating objects based on area
     */

    public function findByPostcodeArea($area): ?PostcodeRating
    {
        return $this->findOneBy(["postcodeArea"=>$area]);
    }
}

AbiCodeLookup

<?php
namespace AppService;

class AbiCodeLookup
{
    public static function getAbiCode(string $regNumber){

        /**
         * create a request to third party api which would return abi code
         * How to configure this service to be used only with regNo factor
         */
        return "22529902";
    }
}

QuoteCalculator

<?php
    namespace AppService;
    use AppEntityRatingFactorInterface;
    
    /**
     * Class QuoteCalculator
     */
    class QuoteCalculator
    {
        /**
         * @param array $ratingFactors
         * @return float
         */
        public static function calculate(array $ratingFactors): float
        {
            $premiumTotal = 500;
            foreach ($ratingFactors as $ratingFactor){
                $premiumTotal = $premiumTotal * ($ratingFactor instanceof RatingFactorInterface ? $ratingFactor->getRatingFactor() : 1);
            }
            return $premiumTotal ;
        }
    
    }

Source: Symfony Questions

Was this helpful?

0 / 0

Leave a Reply 0

Your email address will not be published. Required fields are marked *