CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

PHP Quality Assurance Tools with CakePHP

In Software Development, we are always looking for resources to improve the products we deliver. In this article we show some awesome php qa tools to help us improve our CakePHP software quality and development process. Let's dive in.

Note: The snippets provided in this article can be found at https://github.com/CakeDC/phpqa-app-example
 

Coding Standards

Choosing a coding standard for your project will help the developers team speak the same language by defining a set of rules on how to name variables, methods, class and etc. The coding standard usage can make the integration of community libs and plugins easier.

Checking and fixing coding standards are easily done with the awesome tool PHP_CodeSniffer, this tool has two main scripts:

  • phpcs check for coding standard violations

  • phpcbf autofix coding standard violations, keep in mind that not all errors will be autofixed, but will help you a lot.
     

CakePHP offers a customized coding standards at https://github.com/cakephp/cakephp-codesniffer and you should be using it.
 

Let’s install the tool and the coding standard:

composer require --dev cakephp/cakephp-codesniffer:~4.1.0


Create the file phpcs.xml

<?xml version="1.0"?>

<ruleset name="App">

   <config name="installed_paths" value="../../cakephp/cakephp-codesniffer"/>

   <rule ref="CakePHP"/>

</ruleset>


Update “scripts” block in composer.json  with:

"scripts": {

        "cs-check": "phpcs -p ./src ./tests",

        "cs-fix": "phpcbf -p ./src ./tests",
 

Now we can run composer cs-check and composer cs-fix. In a sample app I got this output saying some errors can be autofixed with composer cs-fix


Static Analysis

How many times have you deployed a project on a production server and found a silly error like calling an undefined method? I hope not often. To avoid this type of issue, a static analysis tool is useful. I recommend you to use PHPStan and PSalm, these tools will help you find errors without having to run the application.

PHStan

PHPStan will rely on phpdoc, so that’s one more reason to use a coding standard tool like PHP_CodeSniffer. Also I recommend that you use the plugin: cakephp-ide-helper, to update annotations (phpdoc) in your app classes.

In some cases we need to help PHPStan understand our code better, and for this reason we created a custom extension: https://github.com/cakedc/cakephp-phpstan.

Let’s install PHPStan using composer:

composer require --dev phpstan/phpstan phpstan/extension-installer cakedc/cakephp-phpstan:^1.0
 

We also need to create two config files

Includes:

    - vendor/cakedc/cakephp-phpstan/extension.neon

    - phpstan-baseline.neon

parameters:

    level: 6

    checkMissingIterableValueType: false

    checkGenericClassInNonGenericObjectType: false

    autoload_files:

        - tests/bootstrap.php

    ignoreErrors:

services:

parameters:

    ignoreErrors:
 

And add two scripts into “scripts” block from composer.json

"stan": "phpstan analyse src/",

        "stan-rebuild-baseline": "phpstan analyse --configuration phpstan.neon --error-format baselineNeon src/ > phpstan-baseline.neon",


Now we can run composer stan and  composer stan-rebuild-baseline the second one will populate phpstan-baseline.neon to ignore all errors returned in composer stan so only use when all the errors shown should be ignored.

 

Psalm 

Psalm is another awesome static analysis tool, it provides a way to check for errors in your code, and can fix some of them, too. In my experience psalm and phpstan work perfect, so you don’t need to pick one or the other, you can use both.

Let’s install psalm:

composer require --dev  "psalm/phar:~3.11.2"


We also need to create two config files

<?xml version="1.0"?>

<psalm

    allowCoercionFromStringToClassConst="true"

    allowStringToStandInForClass="true"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns="https://getpsalm.org/schema/config"

    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"

    errorBaseline="psalm-baseline.xml"

>
   <projectFiles>

        <directory name="src" />

    </projectFiles>

    <issueHandlers>

        <LessSpecificReturnType errorLevel="info" />

        <RedundantConditionGivenDocblockType errorLevel="info" />

        <TypeCoercion errorLevel="info" />

        <DocblockTypeContradiction errorLevel="info" />

    </issueHandlers>

</psalm>
 

<?xml version="1.0" encoding="UTF-8"?>

<files psalm-version="3.11.2@d470903722cfcbc1cd04744c5491d3e6d13ec3d9">

</files>
 

And add two scripts into “scripts” block from composer.json

"psalm": "psalm.phar --show-info=false src/ ",

        "psalm-rebuild-baseline": "psalm.phar --show-info=false  --set-baseline=psalm-baseline.xml  src/"

Now we can run composer psalm and  composer psalm-rebuild-baseline the second one will populate psalm-baseline.xml to ignore all errors returned in composer stan, so only use this when all the errors shown should be ignored.

When we run composer psalm it may inform you that some errors can be autofixed and which arguments you should use to fix. Normally it will be something like

vendor/bin/psalm.phar --alter --issues=MissingReturnType,MissingClosureReturnType --dry-run


Psalm will only change the file If you remove the --dry-run part.

 

Testing

Testing is extremely important for any application development. There are many types of software for testing, in this article we are focusing on unit tests. Unit tests help us check if one specific piece of code is working as expected, you can think like ‘Is method ‘A’ returning the correct value when argument has value ‘Z’?’.

In CakePHP we have built-in support to PHPUnit, CakePHP integration offers additional features to make it easier to run units tests like custom asserts and methods for integration tests, and fixtures logic for models. We can bake unit tests using the bake plugin.

To improve your unit tests try the cakedc/cakephp-db-test with this one you can use database fixtures instead of files. Learn more at: https://www.cakedc.com/rafael_queiroz/2020/04/27/unit-testing-with-cakedc-db-test

PHPUnit probably already is installed in your app but if not I recommend you to compare your project files with: https://github.com/cakephp/app.

For more information check out: https://book.cakephp.org/4/en/development/testing.html

 

Hope you enjoyed this article and will start using these awesome tools to make your projects better than ever. Good Baking.

 

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