Pint, Rector, Larastan, and Pest: Essentials for Success

Part of the series Empower and Enhance Your VitePress Blog with a Laravel API

In the world of web development, maintaining code quality and consistency across the entire project is key to success. The more the project grows, the more difficult it becomes to maintain a clean codebase by hand. Fortunately, there are tools available to help us achieve this goal. In this article, we will explore four essential tools for any Laravel project: Pint, Rector, Larastan, and Pest.

  1. Pint is an opinionated PHP code style fixer for minimalists. It helps you maintain a consistent coding style throughout your project by automatically formatting your code according to predefined rules. This not only improves readability but also reduces the chances of introducing bugs due to inconsistent coding practices. In the JS world, we have Prettier or ESLint, and Pint is the equivalent for PHP.
  2. Rector is a tool for automated refactoring of PHP code. It allows you to apply various code transformations and improvements automatically, saving you time and effort. With Rector, you can easily upgrade your code to the latest PHP standards, remove deprecated features, and apply best practices without having to manually edit each file. In the JS world, there is no direct equivalent for Rector, but TypeScript and ESLint can be used to mimic some of its features.
  3. Larastan is a static analysis tool for Laravel applications. It helps you catch potential bugs and issues in your code before they become a problem. By analyzing your codebase, Larastan can identify areas that may lead to runtime errors, allowing you to fix them proactively. This is especially useful in larger projects where it can be challenging to keep track of all the moving parts. In the JS world, we have TypeScript and ESLint, and Larastan is the equivalent for PHP.
  4. Pest is a testing framework for PHP that focuses on simplicity and elegance. It allows you to write clean and expressive tests for your application, ensuring that your code behaves as expected. By using Pest, you can easily create unit tests, feature tests, and even browser tests, all while maintaining a clean and readable syntax. In the JS world, we have Jest, and Pest is the equivalent for PHP.

By integrating these four tools into your Laravel project from the start, you can ensure that your code remains clean, consistent, and nearly bug-free. This will not only save you time and effort in the long run but also make it easier for other developers to understand and contribute to your project. Win-win for everyone!

Pint

Pint comes pre-installed with Laravel, so you don't need to install it manually. But to have the best experience, we need to configure it and add a custom command to format our code.

For the configuration, we will create a pint.json file in the root of our project. This configuration file will allow us to specify rules and exceptions for our coding style, ensuring consistency across the entire codebase.

json
{
  "preset": "laravel",
  "rules": {
    "array_push": true,
    "backtick_to_shell_exec": true,
    "date_time_immutable": true,
    "declare_strict_types": true,
    "lowercase_keywords": true,
    "lowercase_static_reference": true,
    "final_class": true,
    "final_internal_class": true,
    "final_public_method_for_abstract_class": true,
    "fully_qualified_strict_types": true,
    "global_namespace_import": {
      "import_classes": true,
      "import_constants": true,
      "import_functions": true
    },
    "mb_str_functions": true,
    "modernize_types_casting": true,
    "new_with_parentheses": false,
    "no_superfluous_elseif": true,
    "no_useless_else": true,
    "no_multiple_statements_per_line": true,
    "ordered_class_elements": {
      "order": [
        "use_trait",
        "case",
        "constant",
        "constant_public",
        "constant_protected",
        "constant_private",
        "property_public",
        "property_protected",
        "property_private",
        "construct",
        "destruct",
        "magic",
        "phpunit",
        "method_abstract",
        "method_public_static",
        "method_public",
        "method_protected_static",
        "method_protected",
        "method_private_static",
        "method_private"
      ],
      "sort_algorithm": "none"
    },
    "ordered_interfaces": true,
    "ordered_traits": true,
    "protected_to_private": true,
    "self_accessor": true,
    "self_static_accessor": true,
    "strict_comparison": true,
    "visibility_required": true
  }
}

Let's break down the configuration:

  • preset: This option allows you to choose a preset configuration for Pint. In this case, we are using the laravel preset, which comes with a set of recommended rules for Laravel projects.
  • rules: This section contains the specific rules we want to apply to our codebase. Each rule can be set to true or false, depending on whether we want to enable or disable it.
  • array_push: Enforces the use of the array push syntax instead of the array merge syntax.
  • backtick_to_shell_exec: Converts backticks to the shell_exec function.
  • date_time_immutable: Enforces the use of immutable date and time objects.
  • declare_strict_types: Enforces the use of strict types in PHP files.
  • lowercase_keywords: Enforces the use of lowercase keywords in PHP.
  • lowercase_static_reference: Enforces the use of lowercase static references in PHP.
  • final_class: Enforces the use of final classes.
  • final_internal_class: Enforces the use of final classes for internal classes.
  • final_public_method_for_abstract_class: Enforces the use of final public methods for abstract classes.
  • fully_qualified_strict_types: Enforces the use of fully qualified strict types in PHP files.
  • global_namespace_import: Enforces the use of global namespace imports for classes, constants, and functions.
  • mb_str_functions: Enforces the use of multibyte string functions.
  • modernize_types_casting: Enforces the use of modern type casting in PHP.
  • new_with_parentheses: Enforces the use of parentheses when creating new objects.
  • no_superfluous_elseif: Enforces the removal of superfluous elseif statements.
  • no_useless_else: Enforces the removal of useless else statements.
  • no_multiple_statements_per_line: Enforces the use of a single statement per line.
  • ordered_class_elements: Enforces the ordering of class elements, such as traits, constants, properties, and methods.
  • ordered_interfaces: Enforces the ordering of interfaces.
  • ordered_traits: Enforces the ordering of traits.
  • protected_to_private: Enforces the use of private visibility for properties and methods.
  • self_accessor: Enforces the use of self accessors for properties and methods.
  • self_static_accessor: Enforces the use of self static accessors for properties and methods.
  • strict_comparison: Enforces the use of strict comparison operators.
  • visibility_required: Enforces the use of visibility keywords for properties and methods.

Note

If you have any questions about the rules, like why I chose this or that, feel free to ask in the comments.

Now, let's add a custom command to lint our code. We will add a lint command to our composer.json file:

json
{
  "scripts": {
    "lint": "pint"
  }
}

Starting from now, we can run composer lint to lint our code. Automatically, it will lint all the files in the app, config, database, and routes folders to make sure everything is clean and consistent.

You should see something like this:

bash
 composer lint
> pint

  ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓

  ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Laravel
    FIXED   ......................................................................................................................... 26 files, 26 style issues fixed
 app/Http/Controllers/Controller.php                                                                            declare_strict_types, blank_line_after_opening_tag
 app/Models/User.php                                                                               final_class, declare_strict_types, blank_line_after_opening_tag
 app/Providers/AppServiceProvider.php                                                              final_class, declare_strict_types, blank_line_after_opening_tag
 bootstrap/app.php                                                                                              declare_strict_types, blank_line_after_opening_tag
 bootstrap/providers.php                                                                                        declare_strict_types, blank_line_after_opening_tag
 config/app.php                                                                                                 declare_strict_types, blank_line_after_opening_tag
 config/auth.php                                                                                                declare_strict_types, blank_line_after_opening_tag
 config/cache.php                                                                                               declare_strict_types, blank_line_after_opening_tag
 config/database.php                                                                                            declare_strict_types, blank_line_after_opening_tag
 config/filesystems.php                                                                                         declare_strict_types, blank_line_after_opening_tag
 config/logging.php                                                                                             declare_strict_types, blank_line_after_opening_tag
 config/mail.php                                                                                                declare_strict_types, blank_line_after_opening_tag
 config/queue.php                                                                                               declare_strict_types, blank_line_after_opening_tag
 config/services.php                                                                                            declare_strict_types, blank_line_after_opening_tag
 config/session.php                                                                                             declare_strict_types, blank_line_after_opening_tag
 database/factories/UserFactory.php                                          final_class, self_static_accessor, declare_strict_types, blank_line_after_opening_tag
 database/migrations/0001_01_01_000000_create_users_table.php                class_definition, declare_strict_types, blank_line_after_opening_tag, braces_position
 database/migrations/0001_01_01_000001_create_cache_table.php                class_definition, declare_strict_types, blank_line_after_opening_tag, braces_position
 database/migrations/0001_01_01_000002_create_jobs_table.php                 class_definition, declare_strict_types, blank_line_after_opening_tag, braces_position
 database/seeders/DatabaseSeeder.php                                                               final_class, declare_strict_types, blank_line_after_opening_tag
 public/index.php                                                                                               declare_strict_types, blank_line_after_opening_tag
 routes/console.php                                                                                             declare_strict_types, blank_line_after_opening_tag
 routes/web.php                                                                                                 declare_strict_types, blank_line_after_opening_tag
 tests/Feature/ExampleTest.php                                                                                  declare_strict_types, blank_line_after_opening_tag
 tests/TestCase.php                                                                                             declare_strict_types, blank_line_after_opening_tag
 tests/Unit/ExampleTest.php                                                                                     declare_strict_types, blank_line_after_opening_tag

How should you interpret this output? First, you see the name of the file, followed by the rules that were applied to it. If you see a , it means that Pint successfully formatted the file; otherwise, you'll see a simple . if the file was already well formatted.

That's not all! We'll add a second command called test:lint. This script will be used within our CI to ensure everything is properly formatted. Instead of automatically fixing the code, this command will raise an error if the code is not well formatted.

json
{
  "scripts": {
    "lint": "pint",
    "test:lint": "pint --test"
  }
}

Perfect!

Rector

The second tool we will use is Rector, and it naturally complements Pint. While Pint is used to format code, Rector is used to automatically refactor code. For example, the following code:

php
class SomeClass
{
    public function getValue(int $number)
    {
        if ($number) {
            return 100;
        }

        return 500;
    }
}

Would be refactored to this:

php
final class SomeClass
{
    public function getValue(int $number): int
    {
        return $number ? 100 : 500;
    }
}

By applying the ReturnTypeFromStrictTernaryRector rule.

Let's install Rector with Composer:

bash
composer require rector/rector --dev

Create a rector.php configuration file:

php
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([
        __DIR__.'/app',
        __DIR__.'/bootstrap/app.php',
        __DIR__.'/config',
        __DIR__.'/database',
        __DIR__.'/public',
    ])
    ->withPreparedSets(
        deadCode: true,
        codeQuality: true,
        typeDeclarations: true,
        privatization: true,
        earlyReturn: true,
        strictBooleans: true,
    )
    ->withPhpSets();

This configuration file is a bit more complex than Pint's. Let's break it down:

  • declare(strict_types=1): This line enables strict typing in the file, ensuring that PHP enforces type declarations.
  • use Rector\Config\RectorConfig: This line imports the RectorConfig class, which is used to configure Rector.
  • return RectorConfig::configure(): This line starts the configuration process for Rector.
  • ->withPaths([...]): This method specifies the paths to the directories and files that Rector should analyze and refactor. In this case, we are including the app, bootstrap/app.php, config, database, and public directories.
  • ->withPreparedSets(...): This method allows you to enable predefined sets of rules for Rector. In this case, we are enabling several sets:
    • deadCode: Removes dead code from the project.
    • codeQuality: Improves code quality by applying various best practices.
    • typeDeclarations: Adds type declarations to functions and methods.
    • privatization: Makes properties and methods private when possible.
    • earlyReturn: Applies early return patterns to improve readability.
    • strictBooleans: Enforces strict boolean comparisons.
  • ->withPhpSets(): This method enables a set of rules specifically designed for PHP.

Now, let's add a custom command to our composer.json file:

json
{
  "scripts": {
    "refactor": "rector"
  }
}

Finally, we can run composer refactor to refactor our code. It will automatically refactor all the files in the app, bootstrap/app.php, config, database, and public folders to ensure everything is clean and consistent. You should see something like this:

bash
 composer refactor
> rector
 20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
8 files with changes
====================

1) database/migrations/0001_01_01_000001_create_cache_table.php:12

    ---------- begin diff ----------
@@ @@
      */
     public function up(): void
     {
-        Schema::create('cache', function (Blueprint $table) {
+        Schema::create('cache', function (Blueprint $table): void {
             $table->string('key')->primary();
             $table->mediumText('value');
             $table->integer('expiration');
         });

-        Schema::create('cache_locks', function (Blueprint $table) {
+        Schema::create('cache_locks', function (Blueprint $table): void {
             $table->string('key')->primary();
             $table->string('owner');
             $table->integer('expiration');
    ----------- end diff -----------

Applied rules:
 * AddClosureVoidReturnTypeWhereNoReturnRector
#  ...

Note

I cut the output for brevity, but you should see a list of all the files that were modified.

The output shows the files that were modified with a diff of the changes and the different rules that were applied to the file. Easy and so powerful!

But that's not all! Once again, we will add a second command called test:refactor. This script will be used within our CI to make sure everything is well refactored. So, instead of automatically refactoring the code, this command will just raise an error if the code is not well refactored.

json
{
  "scripts": {
    "refactor": "rector",
    "test:refactor": "rector --dry-run"
  }
}

Larastan

Install Larastan with Composer:

bash
composer require --dev "larastan/larastan:^3.0"

Create a phpstan.neon file:

yml
includes:
  - vendor/larastan/larastan/extension.neon
  - vendor/phpstan/phpstan/conf/bleedingEdge.neon

parameters:
  level: 6

  paths:
    - app
    - config
    - bootstrap
    - database/factories
    - routes

This configuration file is a bit more complex than Pint's. Let's break it down:

  • includes: This section includes additional configuration files that extend the functionality of Larastan. In this case, we are including the larastan/larastan/extension.neon file and the phpstan/phpstan/conf/bleedingEdge.neon file.
  • parameters: This section contains the main configuration parameters for Larastan.
  • level: This parameter sets the level of strictness for the analysis. Higher levels will catch more potential issues but may also produce more false positives. In this case, we are using level 6, which is a good balance between strictness and usability. The stricter the level, the more errors you will get and the more time you will need to spend fixing them. Honestly, I recommend starting with level 2 and progressively, as you get more comfortable with Larastan and its rules, increase the level.
  • paths: This parameter specifies the paths to the directories and files that Larastan should analyze. In this case, we are including the app, config, bootstrap, database/factories, and routes directories.

Now, let's add a custom command to our composer.json file:

json
{
  "scripts": {
    "test:types": "phpstan analyse"
  }
}

Unlike Pint and Rector, Larastan isn't automatic and can't fix the code for you. Instead, it will raise errors and warnings that you need to fix manually.

We can run composer test:types to analyze our code. Automatically, it will analyze all the files in the app, config, bootstrap, database/factories, and routes folders to make sure everything is clean and consistent. You should see something like this:

bash
 composer test:types
> phpstan analyse
Note: Using configuration file /Users/esoub/dev/p/mimram/phpstan.neon.
 20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ---------------------------
  Line   routes/console.php
 ------ ---------------------------
  :9     Undefined variable: $this
         🪪  variable.undefined
 ------ ---------------------------

 [ERROR] Found 1 error

Script phpstan analyse handling the test:types event returned with error code 1

Oh no! We have an error in our routes/console.php file. To fix it, we can simply remove the custom command from the file as we don't need it.

php
<?php

use Illuminate\Foundation\Inspiring; 
use Illuminate\Support\Facades\Artisan; 

Artisan::command('inspire', function () { 
    $this->comment(Inspiring::quote()); 
})->purpose('Display an inspiring quote'); 

Now, let's run composer test:types again:

bash
 composer test:types
> phpstan analyse
Note: Using configuration file /Users/esoub/dev/p/mimram/phpstan.neon.
 20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 [OK] No errors

Pest

The last tool we will use is Pest. Pest is a testing framework for PHP that focuses on simplicity and elegance. It allows you to write clean and expressive tests for your application, ensuring that your code behaves as expected. Pest is the default testing framework in any new Laravel project, so you don't need to install it manually.

Instead, we will configure it and add some custom commands to our composer.json file.

First, let's edit the phpunit.xml file:

xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Http">
            <directory>tests/Http</directory>
        </testsuite>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>app</directory>
            <directory>config</directory>
            <directory>routes</directory>
        </include>
    </source>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="APP_MAINTENANCE_DRIVER" value="file"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_STORE" value="array"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="PULSE_ENABLED" value="false"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

This configuration file is a bit more complex than Pint's. Let's break it down:

  • <?xml version="1.0" encoding="UTF-8"?>: This line specifies the XML version and encoding.
  • <phpunit ...>: This line starts the PHPUnit configuration.
  • xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance": This attribute specifies the XML namespace for the schema instance.
  • xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd": This attribute specifies the location of the XML schema for PHPUnit.
  • bootstrap="vendor/autoload.php": This attribute specifies the bootstrap file for PHPUnit, which is responsible for loading the necessary dependencies.
  • colors="true": This attribute enables colored output in the console.
  • <testsuites>: This section defines the test suites for the project.
  • <testsuite name="Http">: This line defines a test suite named "Http".
  • <directory>tests/Http</directory>: This line specifies the directory containing the tests for the "Http" test suite.
  • <testsuite name="Unit">: This line defines a test suite named "Unit".
  • <directory>tests/Unit</directory>: This line specifies the directory containing the tests for the "Unit" test suite.
  • <source>: This section defines the source directories for the project.
  • <include>: This section includes the specified directories in the source.
  • <directory>app</directory>: This line specifies the app directory as a source directory.
  • <directory>config</directory>: This line specifies the config directory as a source directory.
  • <directory>routes</directory>: This line specifies the routes directory as a source directory.
  • <php>: This section defines the PHP environment variables for the project.
  • <env name="APP_ENV" value="testing"/>: This line sets the APP_ENV environment variable to testing.
  • <env name="APP_MAINTENANCE_DRIVER" value="file"/>: This line sets the APP_MAINTENANCE_DRIVER environment variable to file.
  • <env name="BCRYPT_ROUNDS" value="4"/>: This line sets the BCRYPT_ROUNDS environment variable to 4.
  • <env name="CACHE_STORE" value="array"/>: This line sets the CACHE_STORE environment variable to array.
  • <env name="DB_CONNECTION" value="sqlite"/>: This line sets the DB_CONNECTION environment variable to sqlite.
  • <env name="DB_DATABASE" value=":memory:"/>: This line sets the DB_DATABASE environment variable to :memory:.
  • <env name="MAIL_MAILER" value="array"/>: This line sets the MAIL_MAILER environment variable to array.
  • <env name="PULSE_ENABLED" value="false"/>: This line sets the PULSE_ENABLED environment variable to false.
  • <env name="QUEUE_CONNECTION" value="sync"/>: This line sets the QUEUE_CONNECTION environment variable to sync.
  • <env name="SESSION_DRIVER" value="array"/>: This line sets the SESSION_DRIVER environment variable to array.
  • <env name="TELESCOPE_ENABLED" value="false"/>: This line sets the TELESCOPE_ENABLED environment variable to false.

All these variables are super important to make sure that the tests will run smoothly and in a predictable environment. This limits the flakiness of the tests and simplifies the infrastructure to make them run.

Because we've changed the folder for our integration tests from Feature to Http, we need to rename the tests/Feature folder to tests/Http.

bash
mv tests/Feature/** tests/Http/** && sed -i '' 's/Feature/Http/g' tests/Pest.php

In the tests folder, we now have two folders and two files:

  1. tests/Http: This folder contains the integration tests for the application.
  2. tests/Unit: This folder contains the unit tests for the application.
  3. tests/Pest.php: This file contains the Pest configuration.
  4. tests/TestCase.php: This file contains the base test case for the application.

Finally, let's add some custom commands to our composer.json file:

json
{
  "scripts": {
    "test:type-coverage": "pest --type-coverage --min=100",
    "test:unit": "pest --parallel --coverage --min=100",
    "test": [
      "@test:lint",
      "@test:refactor",
      "@test:types",
      "@test:type-coverage",
      "@test:unit"
    ]
  }
}

The commands are:

  • test:type-coverage: This command runs the tests with type coverage and ensures that all types are present everywhere, thanks to the --min=100 option.
  • test:unit: This command runs the unit tests with coverage and ensures that all lines are covered, thanks to the --coverage and --min=100 options.
  • test: This command runs all the tests, including linting, refactoring, type checking, type coverage, and unit tests. This all-in-one command will be used within our CI. If this command passes, our confidence in the code is at its maximum.

To have the command test:type-coverage working, we need to install a plugin for Pest:

bash
composer require pestphp/pest-plugin-type-coverage --dev

Note

To have the coverage working, you'll have to install xdebug.

Final Thoughts

The combo Pint plus Rector is an absolute game changer for any Laravel project. It allows you to focus on writing code instead of formatting it and ensuring that it is written with the latest standards. Honestly, I can't imagine working without it.

Larastan is an absolute must-have to catch potential type errors before they become a problem. It is a tool a little bit more complex to understand, especially the output that can sometimes be a bit cryptic, but it is so powerful.

And finally, Pest makes writing tests so delightful and clean. It clearly reflects the mindset of the Laravel community: simplicity and elegance.

I hope you enjoyed this article and that you learned something new. If you have any questions, feel free to ask in the comments.

PP

Thanks for reading! My name is Estéban, and I love to write about web development.

I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.

If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.

I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!

Reactions

Discussions

Add a Comment

You need to be logged in to access this feature.