lordphnx / ez-cake
A set of CakePHP quality-of-life enhancers for frequently required behavior
Requires
- php: ^8.1
- ext-json: *
- cakephp/authorization: ^3
- cakephp/cakephp: ^5
Requires (Dev)
- illuminate/support: ^10
- phpunit/phpunit: ^8.0
This package is auto-updated.
Last update: 2024-09-28 18:58:33 UTC
README
EZCake is a set of libraries to make common CakePHP tasks less verbose. It consists of a number of sub-projects:
- EasyTest, to assist in mocking ORM Entities for test-cases
- EasyAuth, to assist in writing
isAuthorized(User $user) : bool
methods - 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:
- project1.name will have it's name property set to "ExampleProject"
- 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 theProject
withproject_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
andArgumentCountError
(). 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