fg/parkour
A lightweight Laravel package for chaining steps into clean, readable flows. Parkour keeps multi-stage actions and pipelines simple to write, easy to follow, and effortless to extend, with minimal setup and familiar Laravel conventions.
Install and verify compatibility first. The last tagged release is from 2015, so before wiring it into an app:
composer require fg/parkour
vendor/bin/phpunit vendor/fg/parkour/tests # or clone and run its own test suite
If Composer refuses on a modern PHP runtime, do not blindly --ignore-platform-reqs; either fork with an updated composer.json or vendor the handful of source files you actually need.
Where to look first
README.md — gives the operation index (get/set/has, pluck, indexBy, groupBy, flatten, map/filter/reduce, merge, etc.). It is thin, as expected for a 2015 package.tests/ — the most reliable usage documentation. Read these before trusting the README examples.src/ — small enough to read end-to-end in one sitting; do this once and you'll know every edge case.First use case: reshaping payloads. The highest-value day-to-day use is turning a flat list of rows into lookup structures, and reading/writing nested config without a chain of isset() calls. Typical first snippet (confirm the exact facade/method names against src/ for your installed version):
use Parkour\Arrays;
$rows = [
['id' => 1, 'name' => 'Alice', 'team' => 'red'],
['id' => 2, 'name' => 'Bob', 'team' => 'blue'],
['id' => 3, 'name' => 'Carol', 'team' => 'red'],
];
$byId = Arrays::indexBy($rows, 'id'); // [1 => [...], 2 => [...], 3 => [...]]
$names = Arrays::pluck($rows, 'name'); // ['Alice', 'Bob', 'Carol']
$teams = Arrays::groupBy($rows, 'team'); // ['red' => [...], 'blue' => [...]]
Arrays::get($config, 'services.mailgun.key', $default);
Arrays::set($config, 'services.mailgun.key', env('MAILGUN_KEY'));
If you are already inside a Laravel codebase, benchmark these against Illuminate\Support\Arr and collect() before adopting — most of the same surface exists there.
Wrap it behind a small app-level adapter. Because the package is unmaintained, isolate it so you can swap it for Illuminate\Support\Arr later:
// app/Support/Arr.php
final class Arr
{
public static function get(array $a, string $path, mixed $default = null): mixed
{
return \Parkour\Arrays::get($a, $path, $default);
}
// ... only the methods you actually use
}
Then a single use App\Support\Arr; across the codebase means a future migration is a one-file change.
Dot-path access for config and request data. Use get/set/has/forget for nested config layering (package defaults → env overrides), and for safely pulling values out of untrusted JSON before validation. This replaces the common data_get-style chains and avoids repetitive isset($a['x']['y']['z']) guards.
Pluck + indexBy for O(1) lookups. In Blade views, API resources, and importers, build keyed maps once rather than calling array_filter/array_search per row:
$usersById = Arrays::indexBy($users, 'id');
$usersByMail = Arrays::indexBy($users, 'email');
Grouping for reporting. groupBy is the natural fit for "orders by customer", "events by day", "rows by status" before feeding into a chart or CSV export.
Flattening import data. For CSV/Excel rows and nested form payloads, flatten plus pluck normalizes the shape before handing off to a FormRequest or a DB::table()->insert().
Compose operations into named pipelines. Where the package provides map/filter/reduce, build small reusable transformations rather than inlining loops:
$normalize = fn (array $rows) => Arrays::map(
Arrays::filter($rows, fn ($r) => $r['active']),
fn ($r) => ['id' => (int) $r['id'], 'name' => trim($r['name'])]
);
Keep these in a Support/Transforms namespace so they are unit-testable in isolation.
Use in commands and seeders. Console commands that massage large arrays (importers, report generators, fixture builders) are the sweet spot — you're working with plain arrays, you don't want a Collection dependency, and you can test the transformation with a fixed fixture.
Integration tip. Register the adapter as a singleton only if you later add config (e.g. a path delimiter or a strict/lenient mode). Otherwise a static facade is fine and avoids ceremony.
License is NOASSERTION. GitHub could not match the license file to a known SPDX license. Before shipping commercially, open LICENSE in the repo, confirm the terms, and copy the notice into your NOTICE/third-party attributions. This is a real blocker in many orgs — resolve it before writing code against the package.
Abandoned since October 2015. Expect PHP 8 issues: removed functions (each()), dynamic property deprecations, and old-style type handling. The 0-dependents stat means no one is upstreaming fixes. Mitigations, in order of preference:
app/Support/Vendor/ (the code is small).composer.json platform bump, and point at it via a VCS repository.--ignore-platform-reqs only in dev, never in production CI.Missing-key semantics. Confirm whether get throws, returns null, or returns the supplied default when an intermediate path is absent. Wrap risky reads in has when the distinction matters, and add a unit test that pins the behavior so a future swap doesn't silently change defaults.
Dot-notation edge cases. Keys that literally contain . (some SaaS IDs do) may be misinterpreted as paths. Check whether the package supports escaping or an array-of-keys form, and prefer that form for user-derived keys.
Mutability. Verify whether operations return new arrays or mutate by reference. Don't assume immutability — a function that returns void and mutates will bite you when you thread it through a pipeline. When in doubt, pass a copy: Arrays::map(array_values($rows), $fn).
Type juggling in indexBy/groupBy. PHP converts numeric-string keys to ints. If you indexBy('id') and later do a strict === comparison against "42" from a request, lookups will miss. Cast at the boundary.
Debugging. Because the codebase is tiny, Xdebug step-through of src/ is faster than reasoning from docs. For quick inspection, var_export() the intermediate result rather than print_r — it round-trips as valid PHP and exposes type differences.
Extension points. Most operations are single-purpose classes; subclass or copy rather than patching vendor code. If a registry/dispatcher exists in your version, register custom operations there so they compose with the built-ins. Otherwise a tiny local pipe() helper is enough.
Run its tests as your spec. vendor/bin/phpunit is the fastest way to detect which primitives broke on your PHP version, and porting a failing test is usually quicker than debugging the failure in your own code.
How can I help you explore Laravel packages today?