CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

Integrating Users and ACL plugins in CakePHP

In previous posts, we saw how CakeDC Users plugin can help you to build an application that manages everything related to users: registration, social login, permissions, etc. Recently it has been noted by the team that there are some use cases where a deeper control of permissions is needed - more than is offered in RBAC. Today we’ll go into this using the ACL approach.

ACL or Access Control List, refers to the application using a detailed list of objects to decide who can access what. It can be as detailed as particular users and rows through to specifying which action can be performed (i.e user XX has permissions to edit articles but does not have permissions to delete articles).

One of the big features of ACL is that both the accessed objects; and objects who ask for access, can be organized in trees.

There’s a good explanation of how ACL works in the CakePHP 2.x version of the Book.

ACL does not form part of CakePHP core V 3.0 and can be accessed through the use of the cakephp/acl plugin.

Let’s just refresh the key concepts of ACL:

  • ACL: Access Control List (the whole paradigm)

  • ACO: Access Control Object (a thing that is wanted), e.g. an action in a controller: creating an article

  • ARO: Access Request Object (a thing that wants to use stuff), e.g. a user or a group of users

  • Permission: relation between an ACO and an ARO

For the purpose of this article - we shall use this use case: You are using CakeDC/users plugin and now want to implement ACL in your application.

Installation

Starting with a brand new CakePHP app:

composer selfupdate && composer create-project --prefer-dist cakephp/app acl_app_demo && cd acl_app_demo

We are going to use CakeDC/users and cakephp/acl plugins. In a single step we can install them with composer:

composer require cakedc/users cakephp/acl

Create a DB and set its name and credentials in the config/app.php file of the just created app (in the Datasources/default section). This command can help you out if you are using MySQL:

mysql -u root -p -e "create user acl_demo; create database acl_demo; grant all privileges on acl_demo.* to acl_demo;"

Plugins will be loaded always with the app. Let’s set them on the bootstrap file:

bin/cake plugin load -br CakeDC/Users
bin/cake plugin load -b Acl

Now let’s insert a line in bootstrap.php before Users plugin loading, so cakedc/users will read the configuration from the config/users.php file of our app.

Configure::write('Users.config', ['users']);

This file does not exist yet. The plugin provides a default file which is very good to start with. Just copy it to your app running:

cp -i vendor/cakedc/users/config/users.php config/

Also, let’s copy the permissions file the same way to avoid warnings in our log files:

cp -i vendor/cakedc/users/config/permissions.php config/

We need to change cakedc/users config: remove RBAC, add ACL. In cakephp/acl there’s ActionsAuthorize & CrudAuthorize. We’ll start just using ActionsAuthorize. We will tell ActionsAuthorize that actions will be under the 'controllers/' node and that the users entity will be MyUsers (an override of the Users entity from the plugin).

Edit the Auth/authorize section of config/users.php so that it sets:

        'authorize' => [
            'CakeDC/Auth.Superuser',
            'Acl.Actions' => [
                'actionPath' => 'controllers/',
                'userModel' => 'MyUsers',
            ],
        ],

Add calls to load components both from Acl & Users plugin in the initialize() method in AppController:

class AppController extends Controller
{
    public function initialize()
    {
        parent::initialize();
        
        // (...)
        $this->loadComponent('Acl', [
            'className' => 'Acl.Acl'
        ]);
        $this->loadComponent('CakeDC/Users.UsersAuth');
        // (...)
    }
    
    // (...)
}

Database tables

Some tables are required in the database to let the plugins work. Those are created automatically just by running their own migrations:

bin/cake migrations migrate -p CakeDC/Users
bin/cake migrations migrate -p Acl

One table from the Acl plugin needs to be fixed because Users migration creates users.id as UUID (CHAR(36)) and Acl migrations creates AROs foreing keys as int(11). Types must match. Let’s fix it adapting the aros table field:

ALTER TABLE aros CHANGE foreign_key foreign_key CHAR(36) NULL DEFAULT NULL;

Now, it’s time to set our own tables as needed for our app. Let’s suppose we are developing a CMS app as specified in the CMS Tutorial from the CakePHP book.

Based on the tutorial, we can create a simplified articles table:

CREATE TABLE articles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id CHAR(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT,
    published BOOLEAN DEFAULT FALSE,
    created DATETIME,
    modified DATETIME,
    FOREIGN KEY user_key (user_id) REFERENCES users(id)
);

Note: Specify CHARACTER SET and COLLATE for user_id only if the table CHARACTER SET and COLLATE of the table differ from users.id (than may happen running migrations). They must match.

Roles will be dynamic: admin will be allowed to manage them. That means that they has to be stored in a table.

CREATE TABLE roles (
    id CHAR(36) NOT NULL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    created DATETIME,
    modified DATETIME
);

Association between users and roles bill be belongsTo, so we’ll need a foreign key in the users table instead of a role varchar field:

ALTER TABLE users
    ADD role_id CHAR(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL AFTER role,
    ADD INDEX role_id (role_id),
    ADD FOREIGN KEY (role_id) REFERENCES roles(id);

ALTER TABLE users
    DROP role;

Baking

Time to think about what will be ACOs and AROs. In most cases, Users will be the only AROs. To do that, we need to link the Users entity and table to the ACL plugin. In this case that we are using CakeDC/users plugin, we first need to extend the plugin as it is explained in the docs. We will also add the behavior and parentNode() as shown in the cakephp/acl readme file, so at the end we’ll need to create those files:

src/Model/Entity/MyUser.php:

<?php
namespace App\Model\Entity;

use CakeDC\Users\Model\Entity\User;

/**
 * Application specific User Entity with non plugin conform field(s)
 */
class MyUser extends User
{
    public function parentNode() {
        return ['Roles' => ['id' => $this->role_id]];
    }
}

src/Model/Table/MyUsersTable.php:

<?php
namespace App\Model\Table;

use CakeDC\Users\Model\Table\UsersTable;

class MyUsersTable extends UsersTable
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->addBehavior('Acl.Acl', ['requester']);
        
        $this->belongsTo('Roles');
        $this->hasMany('Articles');
    }

}

Run bin/cake bake controller MyUsers (beware of case)

Then, edit the top of src/Controller/MyUsersController.php as:

<?php
namespace App\Controller;

use App\Controller\AppController;
use CakeDC\Users\Controller\Traits\LinkSocialTrait;
use CakeDC\Users\Controller\Traits\LoginTrait;
use CakeDC\Users\Controller\Traits\ProfileTrait;
use CakeDC\Users\Controller\Traits\ReCaptchaTrait;
use CakeDC\Users\Controller\Traits\RegisterTrait;
use CakeDC\Users\Controller\Traits\SimpleCrudTrait;
use CakeDC\Users\Controller\Traits\SocialTrait;

class MyUsersController extends AppController
{
    use LinkSocialTrait;
    use LoginTrait;
    use ProfileTrait;
    use ReCaptchaTrait;
    use RegisterTrait;
    use SimpleCrudTrait;
    use SocialTrait;
    
    // CRUD methods ...

To generate the template files for MyUsers we can run:

bin/cake bake template MyUsers

Next, just let Cake bake all objects for articles and roles:

bin/cake bake all Articles
bin/cake bake all Roles

Add behavior to their tables. ArticlesTable will act as controlled because it will represent ACOs:

class ArticlesTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);
        
        // (...)
        $this->addBehavior('Acl.Acl', ['controlled']);
        // (...)

The case of RolesTable will be similar but it will act as requester, as it will represent AROs:

class RolesTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);
        
        // (...)
        $this->addBehavior('Acl.Acl', ['requester']);
        // (...)

Create the parentNode() method in both entities: Article and Role.

    public function parentNode() {
        return null;
    }

Testing

Ok, time to test the whole system! At this point, the app should be ready to use. At least, for an administrator. Let’s quickly create one: it is as easy as running bin/cake users add_superuser. New credentials will appear on screen.

When accessing our app in the URL that we installed it, a login form will appear. Log as the just created admin.

First, let’s create some roles. Go to /roles in your app’s URL. Then, click on "New Role". Create the roles:

  • Author
  • Editor
  • Reader

Then, we can create two users an author and a reader. Head to /my-users and add them. Remember to select the Active checkbox and the proper role in the dropdown menu.

Because MyUsers has the AclBehavior, AROs has been automatically created while creating users, along with the created roles. Check it out with bin/cake acl view aro

Aro tree:
---------------------------------------------------------------
  [1] Roles.24c5646d-133d-496d-846b-af951ddc60f3
    [4] MyUsers.7c1ba036-f04b-4f7b-bc91-b468aa0b7c55
  [2] Roles.5b221256-0ca8-4021-b262-c6d279f192ad
  [3] Roles.25908824-15e7-4693-b340-238973f77b59
    [5] MyUsers.f512fcbe-af31-49ab-a5f6-94d25189dc78
---------------------------------------------------------------

Imagine that we decided that authors will be able to write new articles and readers will be able to view them. First, let’s create the root node for all controllers:

bin/cake acl create aco root controllers

Then, let’s inform ACL that there are such things as articles:

bin/cake acl create aco controllers Articles

Now, we will tell that there are 5 actions related to Articles:

bin/cake acl create aco Articles index

bin/cake acl create aco Articles view

bin/cake acl create aco Articles add

bin/cake acl create aco Articles edit

bin/cake acl create aco Articles delete

We can see the first branch of the ACOs tree here:

bin/cake acl view aco

Aco tree:
---------------------------------------------------------------
  [1] controllers
    [2] Articles
      [3] index
      [4] view
      [5] add
      [6] edit
      [7] delete
---------------------------------------------------------------

ACL knows that articles can be added, so let’s tell who can do that. We can check which aro.id belongs to role Author with:

mysql> select id from roles where name like 'Author';
+--------------------------------------+
| id                                   |
+--------------------------------------+
| 24c5646d-133d-496d-846b-af951ddc60f3 |
+--------------------------------------+
1 row in set (0.00 sec)

And the same with the Reader role::

mysql> select id from roles where name like 'Reader';
+--------------------------------------+
| id                                   |
+--------------------------------------+
| 25908824-15e7-4693-b340-238973f77b59 |
+--------------------------------------+
1 row in set (0.00 sec)

So, if we look up this id in the bin/cake acl view aro output, it turns out that aro.id 1 is Author and that aro.id 3 is Reader.

If we want to let authors (ARO 1) add articles (ACO 5), we must grant permission to Articles/add to editors by running:

bin/cake acl grant 1 5

And we'll grant readers (ARO 3) view articles (ACO 4) with:

bin/cake acl grant 3 4

Don't forget to grant access to Articles/index for all roles, or nobody would access /articles:

bin/cake acl grant 1 3

bin/cake acl grant 2 3

bin/cake acl grant 3 3

Note: Obviously, it would be easier to set a "super role" which includes the 3 roles and grant access to index to it, but we don't want to add too many steps in this tutorial. You can try it for yourself.

Then, aros_acos table becomes:

mysql> select * from aros_acos;
+----+--------+--------+---------+-------+---------+---------+
| id | aro_id | aco_id | _create | _read | _update | _delete |
+----+--------+--------+---------+-------+---------+---------+
|  1 |      1 |      5 | 1       | 1     | 1       | 1       |
|  2 |      3 |      4 | 1       | 1     | 1       | 1       |
|  3 |      1 |      3 | 1       | 1     | 1       | 1       |
|  4 |      2 |      3 | 1       | 1     | 1       | 1       |
|  5 |      3 |      3 | 1       | 1     | 1       | 1       |
+----+--------+--------+---------+-------+---------+---------+
5 rows in set (0.00 sec)

Let’s create a new article as the first user. To do that:

  • Log out (we are still logged in as superadmin) going to /logout
  • Log in as the first created user
  • Go to /articles
  • Create an article

Right now, author can add an article but not view it, since we only set the add permission. Check it out clicking in View next to the article.

Log in as a reader to check how the reader can really view the article.

Obviously, more than a couple of permissions have to be grant in a big app. This tutorial served just as an example to start.

Last words

That's all for now related to the use of ACL in a webapp made with CakePHP. A lot more can be done with ACL. Next step would be to use CrudAuthorize to specify which CRUD permissions are granted for any ARO to any ACO.

Keep visiting the blog for new articles!

This tutorial has been tested with:

  • CakePHP 3.5.10
  • CakeDC/users 6.0.0
  • cakephp/acl 0.2.6

An example app with the steps followed in this tutorial is available in this GitHub repo.

Please let us know if you use it, we are always improving on them - And happy to get issues and pull requests for our open source plugins. As part of our open source work in CakeDC, we maintain many open source plugins as well as contribute to the CakePHP Community.

Reference

Latest articles

How to push Docker image to Container Registry and create App on...

The title speaks for itself, let’s jump right in! As a preliminary step, we start from a user registered in DigitalOcean with a validated account. Use the doctl tool for the entire communication process with DigitalOcean.

Step 1: Install doctl

$ cd ~
$ wget https://github.com/digitalocean/doctl/releases/download/v1.71.0/doctl-1.71.0-linux-amd64.tar.gz
$ tar xvf doctl-1.71.0-linux-amd64.tar.gz
$ sudo mv doctl /usr/local/bin
 

Step 2: Create an API token

  Go to https://cloud.digitalocean.com/account/api/tokens Generate new token for read and write and save apart the value of token generated <TOKEN NAME>: personaltoken 
<TOKEN VALUE>: 6e981fc2a674dbb7a610b9b85d0c8b00

Step 3: Use the API token to grant account access to doctl

$ doctl auth init --context personaltoken
Validating token... OK
Prompt for <TOKEN VALUE>, then enter it and press return 
 

Step 4: Validate that doctl is working

$ doctl auth init  Validating token... OK
  Prompt for <TOKEN VALUE>, then enter it and press return  Validate by obtaining the account information
$ doctl account get Email      Droplet Limit    Email Verified    UUID         Status email@cakedc.com    10     true     5415bbf8-d501-4096-9b75-ab781c017948    active
 

Step 5: Create a Container Registry with doctl

<MY-REGISTRY-NAME> : container-nyc-795 <REGION> : nyc3
$ doctl registry create container-nyc-795 --region nyc3 Name       Endpoint        Region slug container-nyc-795    registry.digitalocean.com/container-nyc-795    nyc3

Important: the region of the Container registry and Kubernetes cluster MUST be the same Keep in mind that container names must be unique, must be lowercase, and only accepts alphanumeric characters and hyphens.  

Step 6: Login to authenticate docker with your registry

$ doctl registry login
Logging Docker in to registry.digitalocean.com
 

Step 7: Create kubernetes cluster

$ doctl kubernetes cluster create cluster-static-example --region nyc3 Notice: Cluster is provisioning, waiting for cluster to be running ................................................................... Notice: Cluster created, fetching credentials Notice: Adding cluster credentials to kubeconfig file found in "/home/andres/.kube/config" Notice: Setting current-context to do-nyc3-cluster-static-example ID                                      Name                      Region    Version        Auto Upgrade    Status     Node Pools d24f180b-6007-4dbc-a2fe-3952801570aa    cluster-static-example    nyc3      1.22.7-do.0    false           running    cluster-static-example-default-pool
Important: the region of the Container registry and Kubernetes cluster MUST be the same here as well.  This operation isn’t a fast process.  

Step 8: Integrate kubernetes cluster in Container register

$ doctl kubernetes cluster registry add cluster-static-example
 

Step 9: Get token certificate and connect to cluster

$ doctl kubernetes cluster kubeconfig save cluster-static-example Notice: Adding cluster credentials to kubeconfig file found in "/home/andres/.kube/config" Notice: Setting current-context to do-nyc3-cluster-static-example
To validate this, use the kubectl tool to get context. If is not installed, get the last version you find - for example in googleapis
$ wget https://storage.googleapis.com/kubernetes-release/release/v1.23.5/bin/linux/amd64/kubectl
$ chmod +x kubectl
$ sudo mv kubectl /usr/local/bin/
Check with
$ kubectl config current-context
do-nyc3-cluster-static-example
Here, you see: bash prompt do-ny3-cluster-sttic-example. That is the context you created in Step 7
 

Step 10: Generate docker image, tag and push to DigitalOcean

We assume that the user already has docker installed
$ mkdir myapp
$ mkdir myapp/html
$ nano myapp/Dockerfile
  Inside Dockerfile put   
FROM nginx:latest
COPY ./html/hello.html /usr/share/nginx/html/hello.htm
l
  We create simple Dockerfile with NGINX Server and copy the file hello.html in the default html of nginx  
$ nano myapp/html/hello.html
  Inside hello.html put  
<!DOCTYPE html> <html>  <head>    <title>Hello World!</title>  </head>  <body>    <p>This is an example of a simple HTML page served from the Nginx container.</p>  </body> </html>
Then build a docker image file tag with repository and push with docker
 
$ cd myapp
$ docker build -t registry.digitalocean.com/container-nyc-795/static-app .
Sending build context to Docker daemon  3.584kB Step 1/2 : FROM nginx:latest latest: Pulling from library/nginx c229119241af: Pull complete 2215908dc0a2: Pull complete 08c3cb2073f1: Pull complete 18f38162c0ce: Pull complete 10e2168f148a: Pull complete c4ffe9532b5f: Pull complete Digest: sha256:2275af0f20d71b293916f1958f8497f987b8d8fd8113df54635f2a5915002bf1 Status: Downloaded newer image for nginx:latest  ---> 12766a6745ee Step 2/2 : COPY ./html/hello.html /usr/share/nginx/html/hello.html  ---> 2b9be913c377 Successfully built 2b9be913c377 Successfully tagged registry.digitalocean.com/container-nyc-795/static-app:latest   $ docker images REPOSITORY                                               TAG       IMAGE ID       CREATED         SIZE registry.digitalocean.com/container-nyc-795/static-app   latest    2b9be913c377   2 minutes ago   142MB   $ docker push registry.digitalocean.com/container-nyc-795/static-app Using default tag: latest The push refers to repository [registry.digitalocean.com/container-nyc-795/static-app] ac03ae036a53: Pushed ea4bc0cd4a93: Pushed fac199a5a1a5: Pushed 5c77d760e1f4: Pushed 33cf1b723f65: Pushed ea207a4854e7: Pushed 608f3a074261: Pushed latest: digest: sha256:22615ad4c324ca5dc13fe2c3e1d2d801bd166165e3809f96ed6a96a2b2ca2748 size: 1777


Step 11: Create app

Create file example-static-app.yaml and insert: alerts:                                 
- rule: DEPLOYMENT_FAILED               
- rule: DOMAIN_FAILED                   
name: example-static-app                
region: nyc                             
services:                               
- http_port: 80                         
  image:                                
    registry_type: DOCR                 
    repository: static-app              
    tag: latest                         
  instance_count: 2                     
  instance_size_slug: professional-xs   
  name: static-service                  
  routes:                               
  - path: /                             
  source_dir: /             
              Validate file with
$  doctl apps spec validate example-static-app.yaml
Create app
$ doctl apps create --spec example-static-app.yaml
Notice: App created
ID        Spec Name             Default Ingress    Active Deployment ID    In Progress Deployment ID    Created At     Updated At
55a7cb68-65b7-4ff1-b6af-388cdb1df507    example-static-app   2022-03-30 09:34:01.288257225 +0000 UTC    2022-03-30 09:34:01.288257225 +0000 UTC
If you access https://cloud.digitalocean.com/apps And in the live url + /hello.html you can see:

Step 12: Deploy app

If you modify the code of your app, you need to generate a new image with docker and push  (see step 10). Then you don’t need to create a new app, you need to deploy the image in the already created app, with the id executing the command to deploy in bash.
$  doctl apps create-deployment 55a7cb68-65b7-4ff1-b6af-388cdb1df507

 

How to generate deploy of Docker image to Container Registry on DigitalOcean Platform Apps

Once you have created the application (and if you have the code in gitlab), you can create a direct deployment of your code in the DigitalOcean container and deploy on top of your application.

Step 1: Define variables

First two variables are defined in gitlab. You can find these inside project in the left menu - enter in Settings > CI/CD > Variables $DIGITALOCEAN_API_KEY = token generated in DigitalOcean dashboard $APP_ID = previously generated application identifier   More can be defined as the name of the repository. The value of these variables will be injected into the file that we will create below in the Step 3  

 

Step 2: Register runner in gitlab

In your project in the left menu, go to Settings > CI/CD > Runners Create a specific runner for the project in the URL with registration token. Register GitLab Runner from the command line. It is important to use docker and privileged
# Download the binary for your system
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# Give it permission to execute
sudo chmod +x /usr/local/bin/gitlab-runner

# Register runner
sudo gitlab-runner register -n --url https://git.cakedc.com --registration-token GR1348941gx7sgV3pZFQgRqg5qUR_ --executor docker --description "My Docker Runner" --docker-image "docker:19.03.12" --docker-privileged --docker-volumes "/certs/client"
 

Step 3: Create file gitlab-ci.yml 

The gitlab-ci.yml file takes care of

  • Authentication and identification using doctl in DigitalOcean
  • Generating and sending the docker image to the DigitalOcean container
  • Deploying the container image to an existing app
Create the file gitlab-ci.yml in the root of your project as
image: docker:20-dind

variables:
  DOCKER_HOST: tcp://docker:2375
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""
  REPOSITORY_URL: registry.digitalocean.com/container-nyc-795/static-app
  CONTAINER_NAME: static-app

services:
  - name: docker:20-dind
    alias: docker
    command: ["--tls=false"]

before_script:
  - apk update
  - apk upgrade
  - apk add doctl --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community
  - docker info

build:
  stage: build
  script:
    - doctl auth init --access-token $DIGITALOCEAN_API_KEY
    - doctl account get     
    - echo $DIGITALOCEAN_API_KEY | docker login -u $DIGITALOCEAN_API_KEY --password-stdin registry.digitalocean.com
    - docker build -t $REPOSITORY_URL:latest .
    - docker push $REPOSITORY_URL
    - doctl apps create-deployment $APP_ID
Now, every time you do a git push in your project, the runner will automatically inject the variables defined previously in the gitlab-ci.yml file. Then, it will generate a docker image with docker (docker-in-docker) to create an image of your project, send it to the digitalocean repository and deploy it in the app configured.   That’s it!  

CakeFest 2021 Recap

Here we are again coming off of the CakeFest sugar high! I don’t even know where to begin.    Unfortunately, or fortunately - I haven’t decided, we had to do another virtual event. The safety of speakers, staff and attendees is very important to us, so a physical event was not the best option in our opinion with traveling.    However, after this event, I started thinking about the people who were able to attend from the comfort of their own homes or offices. These people may not have been able to travel or attend otherwise, and that gives me our silver lining. Not to mention that we had more ticket sales this year than any of our previous events (at least that I can remember).    The theme, for me anyway, ha ha, was traveling the world, ironically. We started in the Canary Islands, traveled to Germany, to Canada, to England and Austria. We had new faces from the US, the Czech Republic and even Japan - and more! This is, as I’ve mentioned, one of the best things about the CakePHP community, we have community members all over the world. This was our chance to come together.    So let’s get to the event. Here’s what you may have missed: 

Workshops:

Workshop 1 Jorge González (Twitter: @steinkelz) Topics covered included: 0:00 - Docker development environment for CakePHP 15:56 - Middlewares  30:05 - Security 1:31:36 - Performance optimization 2:04:49 - Events   Workshop 2 Michael Hoffmann (Twitter: @cleptric) Topics covered included: 0:00:00 -Setup login action in CakePHP 0:29:10 - Vite with hot reloading Vue.js tailwind css   Workshop 3 Mark Story (Twitter: @mark_story) Topics covered included: 0:04:25 - Leveraging new style fixtures 0:48:26 - Using the DI container 1:30:13 - Browser automation testing with Panther. 2:17:13 - Helpers you may need.
 

Talks:

* Juan Pablo Ramirez (Twitter: @jpramidev) gave the keynote talk on behalf of Passbolt. * Sho Ito (Twitter: @itosho) taught us all about Components * Yuki Kanazawa (Twitter: @yakitori009) and this talk about Automatically Distributing Reference Queries to    Read Replica in CakePHP4 * Mark Scherer (Twitter: @dereuromark) schooled attendees on IDE in CakePHP development * Jiri Havlicek (Twitter: @Jerryhavl) played a big role in fighting COVID-19 by helping create a  contact tracing app (developed with CakePHP) in Czech Republic * Chris Miller (Twitter: @ccmiller2019) explained standards and why we use them * Kevin Phifer (Twitter: @lordsimal)  joined in to explain how to re-use code - utility classes and PHP namespaces * Paul Henriks created a plugin with attendees LIVE * Ed Barnard (Twitter: @ewbarnard) brought the dragons! He talked about finding the Joy in Software Development * Chris Hartjes (Twitter: @grmpyprogrammer) delivered a Grumpy Programmer's Guide to being a senior developer  * Joe Ferguson (Twitter: @joepferguson) shared his knowledge on Modern Infrastructure as code with Ansible * Timo Stark (Twitter: @linux_lenny) shared details about NGINX Unit - and how to modernize your CakePHP deployments

Trivia and giveaways 

Cake ceremony dedicated to Mark Story

We took this time to thank and acknowledge Mark Story for all of his hard work and dedication that he puts into CakePHP. He then headed the cake cutting ceremony (virtually of course) as speakers and attendees enjoyed their own treats!   See the full archive here: https://cakefest.org/archive/virtual-2021  

So what’s to come? 

First!  Videos are starting to be released. With the help of community member Aroop Roelofs, we will be releasing these videos faster than expected. Ticket holders have been receiving access, and they will be released publicly in the coming days.  In regards to future events, it’s up in the air. We will have some internal discussions about safety measures and restrictions, then we will weigh the option between another virtual or physical event. We will, of course, reach out to the community for their input.  I will close by just saying THANK YOU. Thank you for making my job worth it. When an event runs smoothly and gets so much great feedback, that is a direct reflection from the community support. We hope you all will continue to join us in years to come!    Thanks for baking!  

Dependency Injection with CakePHP

Let’s talk about Dependency Injection!

SOLID principles

As you know SOLID is an acronym for the  five object-oriented design principles. In this topic, we will focus on Interface segregation principle and Dependency inversion principle. Interface segregation principle states that a client must not be forced to implement an interface that they do not use, or clients shouldn’t be forced to depend on methods they do not use. In other words, having  many client-specific interfaces is better than one general-purpose interface. From the other side, Dependency inversion principle states that objects must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions. To follow Dependency inversion principle, we need to construct low-level modules and pass them to constructors, and that might create a lot of manual work for developers. The dependency injection container is created specifically for solving the problem with manual construction of an object, before creating a specific object. If we follow interface segregation principle when developing application modules, it would be easy to configure a container and switch module dependency. This is where the interface shows its incredible power.  

Few words about CakePHP Events System

CakePHP Events System was created to allow injecting some logic using listeners. However, in some cases, it is used to get results from code that will be created by the module user. When an event is dispatched by the listener, it can return the result. Callback injection through the event system has some drawbacks. First of all, parameters passed to the event need to pass as a hash array. So unfortunately, there is no way to check that all params are really passed or to be sure that all passed params have correct types. Is there a way to solve this problem? Yes, and containers could help with that. Instead of passing events, we can get the required object from the container and call it method. But you could say: wait, we don't know what object could be used in client code within the developed plugin. That's fine, and this  is where interface segregation principle can help. In our plugin, we define an interface for each such case, and instead of dispatching an event, we can easily get an object from the container by interface.       $updater = $container->get(AfterLoginInterface::class);     if ($updater !== null) {         $user = $updater->afterLogin($user);     }   In the Application::services method, users link the interface with the specific class.       public function services(ContainerInterface $container): void     {         $container->add(AfterLoginInterface::class, MyAfterLogin::class);     }   In some of default behavior needed we can map service class for container to default implementation using Plugin::services method.       public function services(ContainerInterface $container): void     {         if (!$container->has(AfterLoginInterface::class)) {             $container->add(AfterLoginInterface::class, NullAfterLogin::class);         }     }  

Container propagation

Dependency injection is an experimental feature. Initial implementation limited by Controllers constructors and methods, and Commands constructors. If we want to access the container in other parts of the application, we may want to propagate it from app level. The most logical way would be to implement middleware and store the container inside the request attribute.   <?php declare(strict_types=1);   namespace App\Middleware;   use Cake\Core\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use RuntimeException;   /**  * Container Injector Middleware  */ class ContainerInjectorMiddleware implements MiddlewareInterface {     /**      * @var \Cake\Core\ContainerInterface      */     protected $container;       /**      * Constructor      *      * @param \Cake\Core\ContainerInterface $container The container to build controllers with.      */     public function __construct(ContainerInterface $container)     {         $this->container = $container;     }       /**      * Serve assets if the path matches one.      *      * @param \Psr\Http\Message\ServerRequestInterface $request The request.      * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.      * @return \Psr\Http\Message\ResponseInterface A response.      */     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface     {         return $handler->handle($request->withAttribute('container', $this->container));     }   That’s it! I hope that this will help you when you are baking with dependency injections. If you run into any problems, there are many support channels that allow the CakePHP community to help  You can check them out under the community tab at CakePHP.org.

We Bake with CakePHP