phine/path
A lightweight PHP library for manipulating file system paths, with helpers for joining, normalizing, resolving, and inspecting paths safely across platforms. Useful for file utilities and framework components that need reliable path handling.
Install it like any other Composer dependency — archived packages remain installable from Packagist:
composer require phine/path
Pin it ("phine/path": "^1.0") and commit the lock file; there will be no upstream fixes, so treat the resolved version as frozen.
Where to look first: src/Path.php. The entire public surface is a single static class, so reading one file gives you the whole API. Skim tests/ alongside it — the test cases are the most accurate spec for edge-case behavior (Windows drive letters, stream wrappers, trailing separators) since documentation is thin.
First use case — sanitize user-supplied paths at a boundary. The highest-value, lowest-risk adoption is normalizing paths you receive from config files, CLI arguments, or HTTP input before you store, compare, or require them:
use Phine\Path\Path;
$raw = $argv[1] ?? './reports/../reports/2024/q1.csv';
if (!Path::isLocal($raw)) {
throw new InvalidArgumentException('Only local paths are allowed.');
}
$clean = Path::canonical(Path::normalize($raw));
// "./reports/../reports/2024/q1.csv" -> "reports/2024/q1.csv"
That is a two-line win over hand-rolled str_replace('//', '/', ...) and explode('/../') juggling, and it is the pattern most day-to-day code actually needs.
Normalize at the edges, trust internally. Convert incoming paths once (config load, request parsing, CLI bootstrap) with normalize() + canonical(), then pass around already-clean strings. Re-canonicalizing everywhere hides bugs instead of fixing them.
Join instead of concatenating. String concatenation with separators is where double-slash and missing-slash bugs live:
// fragile
$file = $baseDir . '/' . $sub . '/' . $name;
// predictable
$file = Path::join($baseDir, $sub, $name);
This matters most when $sub may be empty or already end in a separator.
Guard filesystem operations with isAbsolute() and isLocal(). Before mkdir, unlink, or include, assert the path is absolute so a relative path can't silently resolve against an unexpected CWD, and assert isLocal() so a phar:// or http:// wrapper never reaches a function expecting a real file:
if (!Path::isAbsolute($target) || !Path::isLocal($target)) {
throw new RuntimeException("Refusing to write to {$target}");
}
Use create()/remove() for idempotent scaffolding. Instead of if (!is_dir($x)) mkdir($x, 0777, true); scattered through commands and test fixtures, centralize:
Path::create($cacheDir); // recursive, safe if it already exists
Path::remove($tmpWorkspace); // recursive teardown in test/CLI cleanup
Wrap both in a small Filesystem-style service so the calls are mockable and so you can swap the implementation later (see Gotchas).
Adopt behind an adapter, not directly. Because the API is static and the package is archived, introduce a thin interface in your own codebase:
interface PathHelper
{
public function canonical(string $path): string;
public function isWithin(string $base, string $path): bool;
}
final class PhinePathHelper implements PathHelper
{
public function canonical(string $path): string
{
return Path::canonical(Path::normalize($path));
}
public function isWithin(string $base, string $path): bool
{
$base = $this->canonical($base) . '/';
return str_starts_with($this->canonical($path) . '/', $base);
}
}
A isWithin() containment check built on canonical() is the single most reusable pattern here — it is what stops ../../etc/passwd from escaping your storage root.
Integration in build/tooling code. This is exactly the niche the package was written for: Box2 (the PHAR builder) consumes this fork to resolve and compare paths when assembling archives. If you are writing a similar console/CI tool, canonical() + isLocal() + join() cover path comparison and output-file placement without pulling in a full filesystem abstraction for what is really pure string work.
It is archived — budget for that. No security patches, no PHP-version fixes. Older Phine code tends to be PHP 5/7-era; run it against your target PHP (especially 8.1+) and watch for deprecation notices on implicit nullable parameters and dynamic properties. If it emits noise, you either patch a fork or replace the calls.
canonical() is lexical, not filesystem-aware. It collapses . and .. and separators purely as text. It does not touch the disk: no symlink resolution, no existence check, and .. across a symlinked directory will not match what realpath() returns. If you need the physical path, use realpath(); use canonical() for stable string comparison and storage.
isLocal() is about stream wrappers, not network mounts. It answers "is this a plain filesystem path or does it carry a scheme like phar://, file://, http://". A UNC path or an NFS mount is still "local." Don't use it as a security boundary on its own.
Windows is a separate code path. Drive letters (C:\), UNC prefixes, and backslash-to-slash conversion are handled, but the rules differ from POSIX. If you develop on Linux and deploy on Windows (or vice versa), add explicit CI coverage for isAbsolute() and normalize(); assumptions like "starts with / means absolute" will bite you.
create()/remove() throw Phine\Path\Exception\PathException. Catch it and translate to your domain exception. Do not blanket-suppress — a failed recursive remove() mid-way leaves partial state you probably want to log.
remove() is recursively destructive. canonical() the target first, then assert containment against an expected base directory before calling it. Guarding with a pattern like if (!str_starts_with($path, $allowedRoot . '/')) is cheap insurance against a ..-laden path or an empty string argument.
Empty strings and trailing separators are the usual bugs. canonical(''), join('', ''), and paths ending in / have documented-but-surprising outputs. Add a couple of unit tests around your specific inputs rather than assuming; the package's own tests are the reference.
Static API blocks mocking. There is no instance to inject and no extension point to override — it's a utility class of static methods. That is why the adapter pattern above matters: you mock your interface, not Path. If you need a richer, actively maintained equivalent, symfony/filesystem covers join/canonicalize/mkdir/remove with the same semantics plus an object-oriented API.
Hidden-file detection is name-based. isHidden() looks for a leading dot; it will not report the Windows hidden attribute. Fine for dotfile filtering, wrong for "is this file hidden by the OS."
Tip — pair it with spl_autoload/require carefully. Since canonical() doesn't validate existence, a canonicalized path is safe as a string key but not safe to include. Always confirm existence separately, and prefer a fixed base directory over anything derived from user input.
How can I help you explore Laravel packages today?