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.
- 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.
- 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.
- 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.
- 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.
{
"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 thelaravel
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 totrue
orfalse
, 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 theshell_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:
{
"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:
➜ 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.
{
"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:
class SomeClass
{
public function getValue(int $number)
{
if ($number) {
return 100;
}
return 500;
}
}
Would be refactored to this:
final class SomeClass
{
public function getValue(int $number): int
{
return $number ? 100 : 500;
}
}
By applying the ReturnTypeFromStrictTernaryRector
rule.
Let's install Rector with Composer:
composer require rector/rector --dev
Create a rector.php
configuration file:
<?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 theRectorConfig
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 theapp
,bootstrap/app.php
,config
,database
, andpublic
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:
{
"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:
➜ 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.
{
"scripts": {
"refactor": "rector",
"test:refactor": "rector --dry-run"
}
}
Larastan
Install Larastan with Composer:
composer require --dev "larastan/larastan:^3.0"
Create a phpstan.neon
file:
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 thelarastan/larastan/extension.neon
file and thephpstan/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 theapp
,config
,bootstrap
,database/factories
, androutes
directories.
Now, let's add a custom command to our composer.json
file:
{
"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:
➜ 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
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:
➜ 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 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 theapp
directory as a source directory.<directory>config</directory>
: This line specifies theconfig
directory as a source directory.<directory>routes</directory>
: This line specifies theroutes
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 theAPP_ENV
environment variable totesting
.<env name="APP_MAINTENANCE_DRIVER" value="file"/>
: This line sets theAPP_MAINTENANCE_DRIVER
environment variable tofile
.<env name="BCRYPT_ROUNDS" value="4"/>
: This line sets theBCRYPT_ROUNDS
environment variable to4
.<env name="CACHE_STORE" value="array"/>
: This line sets theCACHE_STORE
environment variable toarray
.<env name="DB_CONNECTION" value="sqlite"/>
: This line sets theDB_CONNECTION
environment variable tosqlite
.<env name="DB_DATABASE" value=":memory:"/>
: This line sets theDB_DATABASE
environment variable to:memory:
.<env name="MAIL_MAILER" value="array"/>
: This line sets theMAIL_MAILER
environment variable toarray
.<env name="PULSE_ENABLED" value="false"/>
: This line sets thePULSE_ENABLED
environment variable tofalse
.<env name="QUEUE_CONNECTION" value="sync"/>
: This line sets theQUEUE_CONNECTION
environment variable tosync
.<env name="SESSION_DRIVER" value="array"/>
: This line sets theSESSION_DRIVER
environment variable toarray
.<env name="TELESCOPE_ENABLED" value="false"/>
: This line sets theTELESCOPE_ENABLED
environment variable tofalse
.
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
.
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:
tests/Http
: This folder contains the integration tests for the application.tests/Unit
: This folder contains the unit tests for the application.tests/Pest.php
: This file contains the Pest configuration.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:
{
"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:
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.
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!
Discussions
Add a Comment
You need to be logged in to access this feature.