CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

Users plugin 9.x for CakePHP4

CakePHP 4 is out for some time (4.0.2 at the time of writing this post) and some of you already asked "Is there a cakedc/users version for cake4?" a couple times...

The answer is YES!.

We had a meeting after we realized AuthComponent was going to be deprecated, the authentication/authorization layer was leaning towards
Middlewares and getting a new Plugin home ourside of the core.
We decided to embrace the new middlewares and provide native compatibility from version 9.x of the plugin.

Before we explain how is it going to work, and proceed to setup a project from scratch a couple answers to potential questions:

* Are you going to maintain cakedc/users 8.x for CakePHP 3.x?
  * Yes, as long as you use it, we'll keep it maintained. Version 8.x will be a LTS version. We'll consider backporting some features from 9.x but the
  main objective of 8.x is to provide stability over time to existing CakePHP 3 projects.
* I'm planning to migrate my project from CakePHP 3 to CakePHP 4, what's the migration path for cakedc/users?
  * Keep the version 8.x of the plugin for some time. Complete your upgrade and then decide if you want to migrate to cakedc/users 9.x.
  We wrote a migration guide here when you decide it's time to move forward.
* AuthComponent is going to explode in CakePHP 4.x ?
  * No, it's deprecated, but deprecated also means it's completely functional. Just keep it until you decide to upgrade it.
 
This blog post is the first of a series of articles about cakedc/users 9.x where we'll explain how to use the plugin from scratch, it's benefits and
also the way the new authentication & authorization layers work for a CakePHP 4.x application.

We've kept our main objectives for this plugin: provide a quick, easy, flexible users management plugin to help you build awesome products on top of CakePHP 4.

So let's create a new project

* Ensure you have a working develop environment, download a development vagrant machine or pick some other's environment.

composer create-project cakephp/app:^4 users9

Now we have a new CakePHP 4 project skeleton created under folder users9.
Test it using bin/cake server then go to http://localhost:8765 and check it's all green.

 

all green checks for cakephp 4

 

* Install the plugin using
composer require cakedc/users:^9

* Now configure your application to use the plugin in your src/Application.php bootstrap() method, add
$this->addPlugin(\CakeDC\Users\Plugin::class);

* Create the required tables in your configured Datasource using Migrations:
bin/cake migrations migrate -p CakeDC/Users

This will create an empty `users` table and `social_accounts` to hold your authenticatio data.

* Now create a superadmin user
bin/cake users addSuperuser

It will create a superadmin user and a random password, copy the user and password to a safe place.

Now start the standalone server using bin/cake server again, and go to the home page http://localhost:8765... you'll see the login form.

 

login form cakephp 4 and cakedc/users


If you use the superadmin credentials previously generated, you'll be able to login and continue to the home page.

Total setup process, possibly less than 5 minutes if your network allows it...

How is it actually working?

* Once the plugin is added to you Application class through the Plugin class, we implement the  `AuthenticationService` and `AuthorizationService` provider interfaces, and use
the `middleware()` plugin callback to inject the configured middlewares into the Application middleware queue, see the `MiddlewareQueueLoader`.
We do it to keep the loading and configuration of the middlewares in one place, and decide the middlewares needed based on your configuration, for example if you are using
social login with Facebook, we configure which middlewares you need loaded and the correct order.
Check LINK for customization options of the plugin. We'll deal with customization in other articles of this series.

* CakePHP core Authentication and Authorization middlewares are also loaded, with the provided configuration. Check file vendor/cakedc/users/config/users.php for the
default configuratio used. You'll see there we're using by default Session, Form, Token, Cookie and Social. Depending on your configuration we'll check if you're authenticated in the following order:
  * Your identity already in the session
  * You've posted login form and your credentials are in the request data
  * There is a token present we can retrieve, usually for API stateless token based auth
  * There is a remember me cookie present
  * Your identity is available after social login, and we can use it to login into the app
 
If all these methods fail, you're redirected to the login page, `/login` by default (configurable, of course).  

* But that's the first step, once we can identify who are you, the next step (Authorization) is to determine if you're allowed to access the page you're trying to open. The plugin default configuration has 2
Authorization methods, superuser and rbac.
  * If you're user is a superuser, you are granted
  * If there is a rule in the rbac configuration to match your role and the current page, you are granted
 
If none of the above, you are not authorized, and redirected to the home page.

One of the important concepts about the new authentication layer in CakePHP is: "Authentication happens before you hit AppController".
So when you get to your Controller, CakePHP alreay knows you're an existing user and you have permission to access the page. All the abstraction and complexity
of maintaining the authentication is now extracted and managed OUTSIDE of your controllers, reducing their complexity.

Give it a try in your next project! Let us know how it goes and share you experiences with us, we'll be happy to help in the community channels.

 

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