lordphnx/ez-cake

A set of CakePHP quality-of-life enhancers for frequently required behavior

9.0.0 2023-10-04 14:51 UTC

This package is auto-updated.

Last update: 2024-04-28 10:01:35 UTC


README

EZCake is a set of libraries to make common CakePHP tasks less verbose. It consists of a number of sub-projects:

  1. EasyTest, to assist in mocking ORM Entities for test-cases
  2. EasyAuth, to assist in writing isAuthorized(User $user) : bool methods
  3. ErrorPrevention, to prevent pesky wordpress-scanners from cluttering your (Sentry) error logs

Why EasyCake

For my company, I was running a lot of different CakePHP Projects, and I kept rewriting the same logic over and over again. So I dediced to put them in a library. Over the course of time, other developer friends started to borrow these libraries, so I felt it was time to just open-source this stuff.

EasyTest

What annoyed me a great deal is that when mocking entities with a lot of fields, you usually only care about one or two. So at the core of the EasyTestTrait is the createGeneric method.

The following code will generate two Project entities with (mostly) random values, except that:

  1. project1.name will have it's name property set to "ExampleProject"
  2. project2.name will have it's name property set to "ExampleProject2"
  • Note that these entities will actually be saved on their according models.
  • EasyTestTrait will introspect the table schema, and generate random values according to each column type
$project1 = $this->genericCreate("Project",[
    "name" => "ExampleProject"
]);
$project2 = $this->genericCreate("Project",[
    "name" => "ExampleProject2"
]);
//Project1
Project {
    "project_id" => 1,
    "name" => "ExampleProject",
    "is_premium" => true,
    "size" => 5 
}

Project {
    "project_id" => 2,
    "name" => "ExampleProject2",
    "is_premium" => false,
    "size" => 17
}

Relations

Sometimes you will have mandatory dependencies, so what I usually do is the following:

Create a project-specific test trait (e.g. ProjectTestTrait.php)

function createUser(array $overrides = []) : User{
    return $this->genericCreate("Users", [
        "password" => hash("sha256","stupidPassword")
    ], $overrides);
}

function createProject(User $owner, array $overrides = []) : Project {
    return $this->genericCreate("Users", [
        "owner_id" => $owner->user_id
    ], $overrides);
}

//create a User entity, and ensure it's is_admin property is set to true
$user = $this->createUser(['is_admin' => true]);
//create a project, using the created user as its owner
$project = $this->createProject($user);

EasyAuth

For me, the most common way to do authorization is the following:

  • Somebody makes a requests, for example, /projects/view/1.
  • What you want to ascertain is if the currently logged in User has a particular relationship to the Project with project_id =1

With EasyAuth, all you have to do is encode that:

  • the first parameter is the identifier of a Project

GET Requests

function isAuthorized(?User $user = null) : bool {
    switch ($this->getRequest()->getAction()) {
        case "view":
            //Here we tell EasyAuth that param 0 should be used to ->get() on the Projects model.
            return  $this->easyAuth([0 => "Projects"], function (Project $project) {
                return $project->isMember($user);            
            }) ;
        default:
            return parent::isAuthorized($user); 
    }
}

This is a less verbose way of saying:

    $action = $this->getRequest()->getAction();
    if ($action === "view") {
        $this->loadModel("Projects");
        try {
            $project = $this->Projects->get($this->getRequest()->getParam(0));
            return $project->isMember($user);
        } catch (Exception e) {
            return false;
        }
    }

This also has the advantage that several actions that share the same authorization logic can just be grouped together

function isAuthorized(?User $user = null) : bool {
    switch ($this->getRequest()->getAction()) {
        case "view":
        case "delete":
        case "edit":
            //Here we tell EasyAuth that param 0 should be used to ->get() on the Projects model.
            return  $this->easyAuth([0 => "Projects"], function (Project $project) {
                return $project->isMember($user);            
            }) ;
        default:
            return parent::isAuthorized($user); 
    }
}

POST Requests

A similar thing to GET requests can be done for POST requests, but the parameters are named after the POST names

function isAuthorized(?User $user = null) : bool {
    switch ($this->getRequest()->getAction()) {
        case "view":
        case "delete":
        case "edit":
            //Here we tell EasyAuth that post-param "project_id" should be used to ->get() on the Projects model.
            return  $this->eastPostAuth(["project_id" => "Projects"], function (Project $project) {
                return $project->isMember($user);            
            }) ;
        default:
            return parent::isAuthorized($user); 
    }
}

ErrorPrevention

I was driven crazy by wordpress vulnerability scanners cluttering my Sentry logs (or any error log for that matter). So I decided to write some basic rules to prevent CakeSentry from logging an error each time we got a ControllerNotFoundException from GET /wp-admin or GET /rpc.php. This turned into an extensible system for easily ignoring errors that you clearly don't care about.

//in Application.php
function buildMiddlewareQueue(MiddlewareQueue $middleWare) : MiddlewareQueue{
    parent::buildMiddlewareQueue($middleWare);
    $errorPrevention = new ErrorPreventionMiddleware();
    
    $errorPrevention->add(WordpressRequests::class);
    $errorPrevention->add(ASPRequests::class);
    $errorPrevention->add(ExternalInvalidUrls::class);
    
    $middleWare->add($errorPrevention);
    return $middleWare;  
}
  • WordPressRequests just ingores every exception that has to do with somebody requesting a wordpress URL
  • ExternalInvalidUrl looks at exceptions that are the result of an invalid request URL (e.g. MissingControllerException and ArgumentCountError(). It then attempts to ascertain if this request was because a link inside our application is faulty, or whether somebody is just manually inputting faulty URLs (e.g. referer = /). In the last case, it's hardly a problem with our application. So the error is not really the fault of our application.
  • ASPRequests ignores every exception that is caused by somebody requesting a .asp file