Introduction
Code formatting is an essential aspect of maintaining clean, readable, and consistent codebases. For PHP developers, PHP-CS-Fixer has become the go-to tool for automatically fixing PHP coding standards issues. But what if you want to provide formatting as a service, integrate it into your CI/CD pipeline, or build a web-based code formatter?
In this comprehensive guide, we'll walk through building a REST API using Laravel that leverages PHP-CS-Fixer to format PHP code. This API will allow clients to submit PHP code and receive properly formatted code in response, following configurable coding standards.
Why Build a Formatter API?
There are several compelling reasons to build a dedicated formatter API:
- Centralized formatting rules: Ensure consistent coding standards across all your projects
- Web-based formatting tools: Power online code editors and formatters
- CI/CD integration: Easily integrate formatting into your continuous integration workflows
- Editor plugins: Create plugins for code editors that don't have native PHP-CS-Fixer support
- Cross-platform compatibility: Provide formatting capabilities regardless of the client's environment
- Custom rule management: Centrally manage and update formatting rules without client-side changes
Prerequisites
Before we begin, make sure you have the following installed:
- PHP 8.1 or higher
- Composer
- Laravel knowledge (basic understanding)
- Git (optional, but recommended)
- A local development environment (Laravel Sail, Laravel Valet, or similar)
Setting Up the Project
Laravel Installation
First, let's create a new Laravel project:
composer create-project laravel/laravel php-formatter-api
cd php-formatter-api
This will create a new Laravel project in the "php-formatter-api" directory and navigate into it.
PHP-CS-Fixer Installation
Next, we'll install PHP-CS-Fixer as a dependency:
composer require friendsofphp/php-cs-fixer --dev
While we're installing it as a dev dependency here, our API will use it as a core component.
API Design
Before diving into implementation, let's design our API. A well-designed API is intuitive, consistent, and follows RESTful principles.
Defining Endpoints
For our formatter API, we'll need the following endpoints:
Endpoint | Method | Description |
---|---|---|
/api/format
|
POST | Format PHP code using default rules |
/api/format/custom
|
POST | Format PHP code using custom rules |
/api/rules
|
GET | List available formatting rules |
/api/presets
|
GET | List available rule presets (PSR-12, Symfony, etc.) |
Request Validation
For our format endpoints, we'll need to validate the incoming requests:
'required|string|max:100000',
'rules' => 'sometimes|array',
'preset' => 'sometimes|string|in:psr12,symfony,laravel',
];
}
}
Response Structure
Our API responses should be consistent and informative. Here's the structure we'll use:
{
"success": true,
"data": {
"formatted_code": "
For error responses:
{
"success": false,
"error": {
"message": "Invalid PHP syntax",
"code": "SYNTAX_ERROR",
"details": {
"line": 5,
"message": "Syntax error, unexpected '}'"
}
}
}
Implementation
Now let's implement our formatter API.
Creating a Formatter Service
First, we'll create a service class to handle the formatting logic:
php artisan make:service PhpCsFixerService
This will create a new service class in{" "}
app/Services/PhpCsFixerService.php
. Let's implement
it:
tempDir = storage_path('app/temp');
if (!file_exists($this->tempDir)) {
mkdir($this->tempDir, 0755, true);
}
}
public function format(string $code, array $rules = [], string $preset = null)
{
// Create a temporary file with the code
$tempFile = $this->tempDir . '/' . md5($code) . '.php';
file_put_contents($tempFile, $code);
// Configure PHP-CS-Fixer
$config = new Config();
if ($preset) {
$config->setRules(['@' . ucfirst($preset) => true]);
} elseif (!empty($rules)) {
$config->setRules($rules);
} else {
// Default rules
$config->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
]);
}
// Set up finder to only target our temp file
$finder = Finder::create()->in($this->tempDir)->name(basename($tempFile));
$config->setFinder($finder);
// Set up differ to generate diff
$differ = new UnifiedDiffer();
// Run the fixer
$errorsManager = new ErrorsManager();
$resolver = new ConfigurationResolver(
$config,
[],
$this->tempDir,
new ToolInfo()
);
$runner = new Runner(
$resolver->getFinder(),
$resolver->getFixers(),
$differ,
null,
$errorsManager,
$resolver->getLinter(),
$resolver->isDryRun(),
$resolver->getCacheManager(),
$resolver->getDirectory(),
$resolver->shouldStopOnViolation()
);
// Get the diff
$changed = $runner->fix();
$diff = $differ->diff($code, file_get_contents($tempFile));
// Clean up
unlink($tempFile);
// Return the formatted code and diff
return [
'formatted_code' => file_get_contents($tempFile),
'diff' => $diff,
'rules_applied' => array_keys($config->getRules()),
];
}
public function getAvailableRules()
{
// This is a simplified version - in a real app, you'd want to
// dynamically extract rules from PHP-CS-Fixer
return [
'array_syntax' => 'Converts array syntax to short or long format',
'braces' => 'Controls brace placement and style',
'class_attributes_separation' => 'Controls spacing between class elements',
'no_unused_imports' => 'Removes unused imports',
'ordered_imports' => 'Sorts imports alphabetically',
// ... more rules
];
}
public function getPresets()
{
return [
'psr12' => 'PSR-12 coding standard',
'symfony' => 'Symfony coding standard',
'laravel' => 'Laravel coding standard',
];
}
}
Building the Controller
Next, let's create a controller to handle our API endpoints:
php artisan make:controller Api/FormatterController
Now, let's implement the controller:
formatterService = $formatterService;
}
public function format(FormatCodeRequest $request)
{
try {
$startTime = microtime(true);
$result = $this->formatterService->format(
$request->input('code'),
$request->input('rules', []),
$request->input('preset')
);
$executionTime = microtime(true) - $startTime;
return response()->json([
'success' => true,
'data' => [
'formatted_code' => $result['formatted_code'],
'diff' => $result['diff'],
],
'meta' => [
'execution_time' => round($executionTime, 3) . 's',
'rules_applied' => $result['rules_applied'],
],
]);
} catch (\\Exception $e) {
return response()->json([
'success' => false,
'error' => [
'message' => 'Failed to format code',
'code' => 'FORMATTER_ERROR',
'details' => [
'exception' => $e->getMessage(),
],
],
], 500);
}
}
public function rules()
{
return response()->json([
'success' => true,
'data' => [
'rules' => $this->formatterService->getAvailableRules(),
],
]);
}
public function presets()
{
return response()->json([
'success' => true,
'data' => [
'presets' => $this->formatterService->getPresets(),
],
]);
}
}
Setting Up Routes
Now, let's define our API routes in routes/api.php
:
Configuration Options
Let's explore how to manage configuration options for our formatter API.
Ruleset Management
PHP-CS-Fixer offers numerous rules that can be enabled, disabled, or configured. For our API, we can create predefined rulesets:
[
'default' => [
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
],
'strict' => [
'@PSR12' => true,
'strict_param' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'declare_strict_types' => true,
],
'laravel' => [
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
],
],
];
Then, update our service to use these configurations:
// In PhpCsFixerService.php
public function format(string $code, array $rules = [], string $preset = null)
{
// ...
if ($preset && config()->has("php-cs-fixer.presets.{$preset}")) {
$config->setRules(config("php-cs-fixer.presets.{$preset}"));
} elseif (!empty($rules)) {
$config->setRules($rules);
} else {
// Default rules
$config->setRules(config('php-cs-fixer.presets.default'));
}
// ...
}
Custom Rules
PHP-CS-Fixer allows for custom rule creation. We can extend our API to support custom rules:
isTokenKindFound(T_DOC_COMMENT);
}
protected function applyFix(\\SplFileInfo $file, Tokens $tokens): void
{
// Implementation of the custom rule
// This is a simplified example
}
public function getName(): string
{
return 'Laravel/comment_style';
}
}
To register custom rules, we'd need to extend the PHP-CS-Fixer configuration in our service.
Testing the API
Testing is crucial for ensuring our API works correctly and remains stable as we make changes.
Unit Tests
Let's create unit tests for our formatter service:
php artisan make:test Services/PhpCsFixerServiceTest --unit
Implement the test:
service = new PhpCsFixerService();
}
public function test_it_formats_php_code()
{
$code = 'service->format($code);
$this->assertEquals($expected, $result['formatted_code']);
}
public function test_it_applies_custom_rules()
{
$code = ' ['syntax' => 'short']];
$result = $this->service->format($code, $rules);
$this->assertStringContainsString('$array = [1, 2, 3];', $result['formatted_code']);
}
public function test_it_returns_available_rules()
{
$rules = $this->service->getAvailableRules();
$this->assertIsArray($rules);
$this->assertNotEmpty($rules);
}
public function test_it_returns_presets()
{
$presets = $this->service->getPresets();
$this->assertIsArray($presets);
$this->assertArrayHasKey('psr12', $presets);
}
}
Feature Tests
Now, let's create feature tests for our API endpoints:
php artisan make:test Api/FormatterControllerTest
Implement the test:
postJson('/api/format', [
'code' => 'assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'formatted_code',
'diff',
],
'meta' => [
'execution_time',
'rules_applied',
],
])
->assertJson([
'success' => true,
]);
$this->assertStringContainsString(
'function test()',
$response->json('data.formatted_code')
);
}
public function test_format_endpoint_handles_invalid_code()
{
$response = $this->postJson('/api/format', [
'code' => 'assertStatus(500)
->assertJsonStructure([
'success',
'error' => [
'message',
'code',
'details',
],
])
->assertJson([
'success' => false,
]);
}
public function test_rules_endpoint_returns_available_rules()
{
$response = $this->getJson('/api/rules');
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'rules',
],
])
->assertJson([
'success' => true,
]);
}
public function test_presets_endpoint_returns_available_presets()
{
$response = $this->getJson('/api/presets');
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'presets',
],
])
->assertJson([
'success' => true,
]);
}
}
Testing with Postman
For manual testing, Postman is an excellent tool. Here's an example of how to set up a request:
-
Create a new POST request to{" "}
http://localhost:8000/api/format
-
Set the Content-Type header to
application/json
-
Add a request body:
{ "code": "
- Send the request and examine the response
Deployment
When you're ready to deploy your formatter API, consider these deployment options:
- Traditional hosting: Deploy on a standard PHP hosting environment with Laravel support
- Docker: Containerize your application for consistent deployment across environments
- Cloud platforms: Deploy to AWS, Google Cloud, or Azure using their managed services
- PaaS: Use Platform-as-a-Service providers like Heroku or Laravel Forge for simplified deployment
Security Considerations
Security is a crucial aspect of any API. Here are some considerations for securing your formatter API:
- Authentication: Implement authentication mechanisms to ensure only authorized clients can access the API
- Rate limiting: Implement rate limiting to prevent abuse and ensure fair usage
- Input validation: Validate all incoming data to prevent injection attacks
- Caching: Implement caching mechanisms to reduce server load and improve performance
- Queue processing: Use queueing systems to handle background tasks and improve scalability
- Performance optimization: Optimize your API for performance to handle high loads