colinodell/indentation
Detect, convert, and normalize whitespace indentation in PHP strings. This small utility identifies whether text uses tabs or spaces, measures indent width, and converts between styles—useful for diffing, formatting, and code generation where consistent indentation matters.
Install with Composer:
composer require colinodell/indentation
There is no service provider, facade, or config file. The package exposes two classes under ColinODell\Indentation:
Indenter — static methods for detecting and rewriting indentation.Indentation — a small value object holding an amount and a type (Indentation::TYPE_SPACE or Indentation::TYPE_TAB).Look at Indenter first; almost every task flows through it.
First use case: make generated output match an existing project file.
use ColinODell\Indentation\Indenter;
$target = Indenter::detectFromFile(base_path('src/ExistingClass.php'));
// e.g. amount=4, type=space
$generated = Indenter::set($generatedCode, $target);
file_put_contents($path, $generated);
A simpler in-memory loop:
$indentation = Indenter::detect($source); // Indentation object
$indentation->getAmount(); // 4
$indentation->getType(); // 'space'
$normalized = Indenter::set($source, new Indentation(2, Indentation::TYPE_SPACE));
$tabs = Indenter::setType($source, Indentation::TYPE_TAB);
$widened = Indenter::setAmount($source, 4);
The Indentation object casts to its literal whitespace via __toString(), which is handy when you need to build prefixes manually:
$unit = (string) Indenter::detect($source); // " " or "\t"
If you only need to remove shared leading whitespace from a heredoc or fixture, use Indenter::stripIndentation($string) — it is the PHP equivalent of a "dedent".
Detect once, apply many times. In a code generator, resolve the project's style from a representative file (or from .editorconfig if you already parse it) and reuse the resulting Indentation object for every emitted file. This avoids per-file detection noise on short snippets.
$style = Indenter::detectFromFile($referenceFile);
foreach ($templates as $template) {
$code = Indenter::set(render($template), $style);
file_put_contents($outputPath, $code);
}
Batch normalization as an Artisan command. The package is a good fit for a one-off "make this directory consistent" command. Detect per file, skip files already matching, and report a diff count.
foreach (File::allFiles($path) as $file) {
$contents = $file->getContents();
$current = Indenter::detect($contents);
$desired = new Indentation(4, Indentation::TYPE_SPACE);
if ((string) $current === (string) $desired) {
continue;
}
$file->put(Indenter::set($contents, $desired));
$this->line("Normalized {$file->getRelativePathname()}");
}
Conform before diffing or storing. When users paste or upload code, normalize indentation before comparing against a canonical version. This prevents whitespace-only diffs from polluting review tools, database snapshots, or checksum comparisons.
Dedent embedded snippets. stripIndentation is ideal for test fixtures and Blade/Artisan output where a snippet is indented for readability in the source but must be asserted or embedded without that leading offset.
$expected = Indenter::stripIndentation('
{
"ok": true
}
');
Laravel integration. Because setFile/detectFromFile require a local path, use the Storage facade for S3 or other disks and keep the package working on strings:
$contents = Storage::get($key);
Storage::put($key, Indenter::set($contents, $style));
For local files inside a Laravel app, Indenter::setFile($path, $style) is a convenient one-liner; pass a third argument to write to a new path instead of overwriting.
Wrap it behind your own service. Detection policy (which reference file, how to aggregate, mixed-style fallback) is application-specific. Put Indenter calls behind a CodeStyle/IndentationResolver class so you can swap detection strategy later without touching callers.
CI guard. Combine detection with a failure condition to fail builds on inconsistent indentation. Since detection is statistical, run it across a sample of files and fail only when a clear majority disagrees with the configured style.
Indentation with your expected value and treat mismatches as failures rather than silently rewriting.set, setType, and setAmount operate line by line. Lines inside heredocs, nowdocs, multi-line strings, Markdown code fences, and YAML blocks will be reindented too, which can change literal values. Do not run this over files where leading whitespace is semantically meaningful to embedded content.setAmount(4, TYPE_TAB) is not meaningful; use setType to switch between tabs and spaces and setAmount only for space-based styles.set on a file containing blank lines before trusting it in bulk rewrites.detect on an unindented string yields amount 0. Guard against dividing by or multiplying by that value if you build your own prefixes.setFile($path, $style) overwrites. Run bulk operations on a clean Git tree and inspect git diff, or pass the optional third argument to write to a separate path first.detectFromFile and setFile use the filesystem directly; they do not understand Laravel disks or stream wrappers beyond what PHP's stream layer supports. Use string methods with Storage::get/put for remote disks.Indenter::set(Indenter::set($x, $style), $style) should equal the first result. Assert this in your own tests, especially around blank lines and heredocs, because that is the property your rewrite pipeline depends on.composer.json and run your own test suite on the PHP versions you deploy. There is no active feature development to rely on for edge cases.new Indentation(4, Indentation::TYPE_SPACE) is clearer and less error-prone than passing ' '; the constants avoid typos between 'space' and 'spaces'..editorconfig per path), implement it in a wrapper that decides when to call Indenter rather than modifying the library.How can I help you explore Laravel packages today?