A tiny, Python pathlib
‑flavoured Path
for PHP that makes path handling and small file ops pleasant and testable.
composer require ohffs/php-pathlib
Before (string wrangling + globals):
$base = rtrim(getenv('HOME'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'myapp';
$cfg = $base . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'settings.json';
@mkdir(dirname($cfg), 0777, true);
file_put_contents($cfg, json_encode(['debug' => true]));
After (fluent, intention‑revealing):
use Ohffs\PhpPathlib\Path;
Path::setAutoExpandTilde(); // optional: call once at bootstrap to expand "~"
$cfg = Path::of('~/myapp')
->joinpath('config', 'settings.json');
$cfg->writeText(json_encode(['debug' => true]));
You get consistent separators, easy joins, and readable code you can test safely.
Heads-up: PHP itself doesn’t expand
~
. Either callexpanduser()
before filesystem operations, or enable auto-expansion in your app (see Behaviour details).
use Ohffs\PhpPathlib\Path;
Path::setAutoExpandTilde(); // optional: expand "~" automatically
// Construct & chain
$project = Path::of('~/projects/example');
$config = $project->joinpath('config', 'app.json');
$readme = $project->joinpath('README.md');
// Queries
$config->exists(); // bool
$config->isFile(); // bool
$project->isDir(); // bool
// Name parts
$readme->name(); // "README.md"
$readme->stem(); // "README"
$readme->suffix(); // "md" (no leading dot)
$project->parent(); // Path("~/projects")
$project->parts(); // absolute parts after expanduser()
// IO
$config->writeText('{"debug":true}');
$text = $config->readText();
// Transformations
$logDir = Path::of('/var/log')->resolve();
$logFile = $logDir->joinpath('app.log').""; // castable to string
$tmpMd = Path::of('report.txt')->withSuffix('md'); // report.md
// Globbing (non‑recursive)
foreach (Path::of('src')->glob('*.php') as $php) {
echo $php->name(), "\n";
}
Sandbox all file ops under a disposable temp directory. Absolute logical paths (like /etc/hosts
) are mapped under the fake base, never touching your real FS.
Pest example
use Ohffs\PhpPathlib\Path;
beforeEach(function () {
$this->fs = Path::fake(); // returns a guard; keep the ref alive
});
afterEach(function () {
unset($this->fs); // or Path::unfake()
});
it('writes without touching the real FS', function () {
$p = Path::of('/app/data.txt');
$p->writeText('ok');
expect($p->exists())->toBeTrue(); // logical
expect(is_file($this->fs->base().'/app/data.txt'))->toBeTrue(); // actual temp
});
Notes
Path::fake()
returns aScopedFake
withbase(): string
and cleans up on__destruct()
.__toString()
always shows the logical path; the temp base is applied only when touching the real filesystem.
Ensure a directory exists and write a file
$dir = Path::of('~/cache/images');
$dir->mkdir(true); // parents=true
$dir->joinpath('index.json')->writeText('[]');
List items in a folder
$names = array_map(fn($p) => $p->name(), Path::of('logs')->iterdir());
Change extension safely (dotfiles supported)
Path::of('/a/archive.tar.gz')->withSuffix('zip'); // /a/archive.tar.zip
Path::of('/a/.env')->withSuffix('bak'); // /a/.env.bak
Normalize a messy path without requiring it to exist
Path::of('/x/y/../z/./a')->resolve(); // /x/z/a
-
~
(tilde) expansion: PHP itself doesn’t expand~
. Either callexpanduser()
before filesystem operations, or enable auto-expansion once in your bootstrap:Path::setAutoExpandTilde(true);
. -
Separators: accepts
/
and\\
in inputs; outputs use your OS separator for real FS calls. Logical string helpers normalise sensibly. -
Dotfiles:
.env
/.gitignore
are treated as no extension;suffix()
returns''
,stem()
returns the whole name. -
glob()
: non‑recursive; uses PHP’sglob()
under the hood; returnsPath[]
with logical paths (no temp prefixes). -
resolve()
: if the actual path exists, usesrealpath()
; otherwise performs a pure string normalisation (.
/..
). -
Safety: in fake mode, even logical absolutes are sandboxed under the temp base.
Path::of(string $path): Path
exists(): bool
isFile(): bool
isDir(): bool
isAbsolute(): bool
name(): string
stem(): string
suffix(): string
(no leading dot)parent(): Path
parts(): string[]
joinpath(string ...$parts): Path
withSuffix(string $suffix): Path
withName(string $name): Path
expanduser(): Path
resolve(): Path
readText(): string
writeText(string $content): void
mkdir(bool $parents = false, int $mode = 0777): void
iterdir(): Path[]
glob(?string $pattern = null): Path[]
Path::fake(): ScopedFake
(returns guard withbase()
; auto‑cleanup on destruct)Path::unfake(): void
- PHP 8.0+ (typed properties & arrow functions)
- No runtime dependencies
MIT — see LICENSE
.