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

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

Now that the Laravel project is in place and its structure is familiar, it is time to make the codebase easier to maintain.

As the project grows, consistent formatting, automated refactoring, static analysis, and tests will help us move quickly without losing confidence. In this article, we will configure four tools that support that workflow: Pint, Rector, Larastan, and Pest.

  1. Pint formats PHP code according to a consistent style. In the JavaScript ecosystem, it is closest to Prettier or ESLint's formatting rules.
  2. Rector refactors PHP code automatically. It can modernize syntax, remove deprecated patterns, and apply project-wide improvements.
  3. Larastan performs static analysis tailored to Laravel. It catches many potential problems before the code runs.
  4. Pest is a testing framework focused on simple, expressive tests. It is the closest PHP equivalent to tools such as Jest in the JavaScript ecosystem.

Using these tools from the start keeps the codebase easier to maintain and gives future contributors a clear, consistent foundation. It also saves time as the project grows.

Pint

Pint comes pre-installed with Laravel, so there is nothing to install. We only need to configure it and add a Composer script for formatting the code.

Create a pint.json file in the project root. This file lets us choose the rules that should be applied across the 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 array push syntax instead of array merging.
  • 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 questions about any of these rules, 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"
  }
}

From now on, we can run composer lint to format the code. Pint will process the files in the app, config, database, and routes folders and keep their style 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

The output shows each file and the rules that Pint applied. A means that Pint changed the file; a . means that the file was already formatted correctly.

We will also add a test:lint command for CI. Instead of changing files, it checks the formatting and exits with an error if a file needs changes.

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

Perfect!

Rector

The second tool is Rector, which complements Pint. Pint changes formatting, while Rector changes the structure and behavior of the code according to its rules. For example:

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 is a little more involved than Pint's. Here is what each part does:

  • 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"
  }
}

We can now run composer refactor. Rector will inspect the files in app, bootstrap/app.php, config, database, and public and apply the configured transformations. You should see output similar to 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

The output is shortened here. Your terminal should list every file that Rector modified.

The output lists each modified file, shows a diff, and identifies the rules that were applied.

We will also add a test:refactor command for CI. It runs Rector in dry-run mode and reports changes without modifying files.

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 tells Larastan what to load and which parts of the project to analyze:

  • 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 strictness of the analysis. Higher levels catch more potential issues but can also produce more false positives. Level 6 is a reasonable balance for this project, but starting at level 2 and increasing it gradually is also a good approach.
  • 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.

Run composer test:types to analyze the files in app, config, bootstrap, database/factories, and routes. Larastan reports problems that must be fixed manually. You should see output similar to 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

The error comes from the default command in routes/console.php, which we do not need for this project. Remove that command to fix 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'); 

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 is Pest, a PHP testing framework focused on simple, expressive tests. Pest is the default testing framework in a new Laravel project, so there is nothing to install.

We will configure it and add a few custom commands to composer.json.

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 defines the test suites, the application source files, and the environment used during tests:

  • <?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.

These variables give the tests a predictable environment, which reduces flakiness and keeps the test setup simple.

Because we changed the integration-test folder from Feature to Http, rename tests/Feature 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"
    ]
  }
}

These commands do the following:

  • test:type-coverage: Runs the tests with type coverage and requires 100% coverage through --min=100.
  • test:unit: Runs the tests with line coverage and requires every line to be covered through --coverage and --min=100.
  • test: Runs linting, refactoring, type checking, type coverage, and the test suite. This is the command we will use in CI.

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

Code coverage requires xdebug.

Final Thoughts

With Pint, Rector, Larastan, and Pest configured, the project now has a repeatable quality workflow. Formatting, refactoring, static analysis, and tests can support us as the codebase grows instead of becoming cleanup work we postpone.

The Laravel foundation is ready, so we can start building features on top of it. Next, we will add GitHub authentication with Socialite so users can identify themselves before they manage comments.

PP

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

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!

Continue readingEffortless Social Authentication with Laravel Socialite

Reactions

Discussions

Add a Comment

You need to be logged in to access this feature.