Format PHP with PHP-CS-Fixer in Zed Editor

The new Code Editor Zed is balzing fast and has language support for PHP, so why not try it out?

Format PHP with PHP-CS-Fixer in Zed Editor
02 Oct 2024
|
2 min read

I recently tried out the new Zed code editor, which also has first party support for PHP Language Servers phpactor or intelliphense!

One issue I ran into is auto formatting code. I'm a fan of auto formatting since this preserves a cohesive code style over many projects and helps to reduce merge issues.

External formatters in Zed

Formatters in Zed work a bit different than some implementations in VSCode or similar editors.

You can configure an external formatter by specifying an external key with a command and arguments values.

Once you run the editor: format action or save the file (with format_on_save enabled), you can provide the formatter with the current file in two ways

  1. Set a {buffer_path} placeholder variable

  2. Append a - to pipe in the file contents to the command

We will use the second way since you could also call commands inside docker containers. When mounting your project root inside a container, the absolute path placed inside {buffer_path} will mismatch and you will need to do some magic to convert it into a relative path.

Format using PHP-CS-Fixer

I've created a bin script in my PHP-CS-Fixer Config project that takes in the STDIN, calls PHP-CS-Fixer to format the file contents and returns the result as STDOUT.

 1{
 2  "languages": {
 3    "PHP": {
 4      "language_servers": ["intelephense", "!phpactor"],
 5      "format_on_save": "on",
 6      "formatter": {
 7        "external": {
 8          "command": "vendor/bin/php-cs-fixer-stdin",
 9          "arguments": ["-"]
10        }
11      }
12    }
13  }
14}

The bin script is pretty opinionated and only uses the fix command of PHPCS, feel free to fork it, contribute or do whatever!

Install

You could just install the PHP-CS-Fixer-Config dependency or copy the script below. Make sure to alter the command path if you wish to use the manual way.

1composer require romanzipp/php-cs-fixer-config --dev

php-cs-fixer-stdin.php

 1// Inspired from https://gist.github.com/vuon9/be16429f751e12f72e220c18777d9bc7
 2//
 3// This script will
 4//   1. Read the file contents provided by STDIN
 5//   2. Create a temporary file (tries multiple directories)
 6//   3. Call PHP-CS-Fixer's "fix" command with path to the temp file
 7//   4. provides the fixed file contents as STDOUT
 8
 9function error_exit(string $message, int $code = 1): void
10{
11    fwrite(STDERR, $message . PHP_EOL);
12    exit($code);
13}
14
15// Read file contents from STDIN
16
17$fileContents = file_get_contents('php://stdin');
18$fileContents = trim($fileContents);
19
20// Create temp file and save STDIN contents
21
22$tryTmpDirs = [
23    sys_get_temp_dir(),
24    '.tmp',
25];
26
27$createdTempFile = false;
28
29foreach ($tryTmpDirs as $tmpDir) {
30    $ok = is_dir($tmpDir) || mkdir($tmpDir, 0777, true);
31    if (false === $ok) {
32        continue;
33    }
34
35    $tmpFile = tempnam($tmpDir, 'fix_');
36    if (false === $tmpFile) {
37        continue;
38    }
39
40    $ok = file_put_contents($tmpFile, $fileContents);
41    if (false === $ok) {
42        continue;
43    }
44
45    $createdTempFile = true;
46}
47
48if ( ! $createdTempFile) {
49    error_exit('could not save STDIN to temp file');
50}
51
52// Check if PHP-CS-Fixer is installed
53
54$whichBinary = exec('which php-cs-fixer');
55if ('' === $whichBinary) {
56    error_exit('php-cs-fixer binary not found in $PATH folders');
57}
58
59$cmd = sprintf('php-cs-fixer --quiet fix %s', $tmpFile);
60
61// Run the command
62
63$output = [];
64$returnCode = 0;
65
66exec($cmd, $output, $returnCode);
67
68if ($returnCode > 0) {
69    error_exit(implode(' ', $output), $returnCode);
70}
71
72// Return new contents to STDOUT
73
74$newContents = file_get_contents($tmpFile);
75
76if (false === $newContents) {
77    error_exit('couldnt read from temp file after fixing');
78}
79
80if ( ! empty($newContents)) {
81    fwrite(STDOUT, trim($newContents) . PHP_EOL);
82}
83
84if (false === @unlink($tmpFile)) {
85    error_exit('couldnt delete temp file');
86}


Read more...