Skip to content

Commit 493a3b3

Browse files
committed
Add comprehensive breadcrumb management support
Introduce `Step2Dev\LazyBreadcrumb` package with enhanced breadcrumb capabilities: - Add Blade components (`breadcrumbs` and `breadcrumbs-json-ld`). - Create commands for managing breadcrumbs (`list`, `test`, `sync`, `make`). - Implement route macros (`breadcrumbs` and `resourceWithBreadcrumbs`). - Add middleware to share breadcrumbs with views. - Extend configuration and default view support. - Provide utilities for JSON-LD rendering and structured data compliance.
1 parent ff59beb commit 493a3b3

File tree

13 files changed

+473
-10
lines changed

13 files changed

+473
-10
lines changed

config/lazy-breadcrumb.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,5 @@
22

33
// config for Step2Dev/LazyBreadcrumb
44
return [
5-
'default' => [
6-
[
7-
'url' => '/',//route('home'),
8-
'label' => env('APP_NAME'),
9-
],
10-
],
5+
'view' => 'breadcrumbs::components.breadcrumbs',
116
];
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<nav class="flex" aria-label="Breadcrumb">
2+
<ol class="inline-flex items-center space-x-1 md:space-x-3">
3+
@foreach ($breadcrumbs as $crumb)
4+
@if (!$loop->first)
5+
<li>
6+
<div class="flex items-center">
7+
<svg class="w-4 h-4 text-gray-400 mx-1" fill="currentColor" viewBox="0 0 20 20">
8+
<path d="M7.05 4.05a.5.5 0 0 1 .7 0l5 5a.5.5 0 0 1 0 .7l-5 5a.5.5 0 0 1-.7-.7L11.29 10 7.05 5.75a.5.5 0 0 1 0-.7z"/>
9+
</svg>
10+
</div>
11+
</li>
12+
@endif
13+
14+
<li class="inline-flex items-center">
15+
@if ($loop->last)
16+
<span class="text-sm font-medium text-gray-500">{{ $crumb['title'] }}</span>
17+
@else
18+
<a href="{{ $crumb['url'] }}" class="text-sm font-medium text-blue-600 hover:underline">
19+
{{ $crumb['title'] }}
20+
</a>
21+
@endif
22+
</li>
23+
@endforeach
24+
</ol>
25+
</nav>

src/Breadcrumbs.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace Step2Dev\LazyBreadcrumb;
4+
5+
use Illuminate\Support\Facades\Route;
6+
use Step2Dev\LazyBreadcrumb\Breadcrumbs\Trail;
7+
use Illuminate\Support\Collection;
8+
9+
10+
class Breadcrumbs
11+
{
12+
protected static array $definitions = [];
13+
14+
public static function for(string $name, \Closure $callback): void
15+
{
16+
static::$definitions[$name] = $callback;
17+
}
18+
19+
public static function generate(?string $name = null, array $params = []): array
20+
{
21+
$name ??= Route::current()?->getName();
22+
if (!$name || !isset(static::$definitions[$name])) {
23+
return [];
24+
}
25+
26+
$trail = new Trail();
27+
static::$definitions[$name]($trail, $params);
28+
return $trail->get();
29+
}
30+
31+
public static function renderJsonLd(?string $name = null, array $params = []): string
32+
{
33+
$items = static::generate($name, $params);
34+
35+
$structured = [
36+
'@context' => 'https://schema.org',
37+
'@type' => 'BreadcrumbList',
38+
'itemListElement' => [],
39+
];
40+
41+
foreach ($items as $i => $crumb) {
42+
$structured['itemListElement'][] = [
43+
'@type' => 'ListItem',
44+
'position' => $i + 1,
45+
'name' => $crumb['title'],
46+
'item' => $crumb['url'],
47+
];
48+
}
49+
50+
return '<script type="application/ld+json">' .
51+
json_encode($structured, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
52+
. '</script>';
53+
}
54+
55+
public static function toArray(?string $name = null, array $params = []): array
56+
{
57+
return static::generate($name, $params);
58+
}
59+
60+
public static function toCollection(?string $name = null, array $params = []): Collection
61+
{
62+
return collect(static::generate($name, $params));
63+
}
64+
65+
public static function has(?string $name = null): bool
66+
{
67+
$name ??= Route::current()?->getName();
68+
return $name && array_key_exists($name, static::$definitions);
69+
}
70+
71+
public static function all(): array
72+
{
73+
return array_keys(static::$definitions);
74+
}
75+
76+
public function import(array $breadcrumbs): static
77+
{
78+
foreach ($breadcrumbs as $crumb) {
79+
$this->breadcrumbs[] = $crumb;
80+
}
81+
return $this;
82+
}
83+
84+
public static function macro(string $name, \Closure $callback): void
85+
{
86+
self::$definitions[$name] = $callback;
87+
}
88+
89+
}

src/Breadcrumbs/RouteMacro.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Route;
4+
use Illuminate\Routing\Route as LaravelRoute;
5+
use Illuminate\Routing\Router;
6+
use Step2Dev\LazyBreadcrumb\Breadcrumbs;
7+
use Step2Dev\LazyBreadcrumb\Breadcrumbs\Trail;
8+
9+
Route::macro('breadcrumbs', function ($callback = null) {
10+
/** @var LaravelRoute $this */
11+
$name = $this->getName();
12+
13+
if ($name && is_callable($callback)) {
14+
Breadcrumbs::for($name, $callback);
15+
}
16+
17+
return $this;
18+
});
19+
20+
Router::macro('resourceWithBreadcrumbs', function (
21+
string $name,
22+
string $controller,
23+
array $options = []
24+
) {
25+
/** @var Router $this */
26+
$routes = $this->resource($name, $controller, $options);
27+
28+
$titles = $options['titles'] ?? [];
29+
30+
$parts = explode('.', $name);
31+
$lastPart = end($parts);
32+
$baseTitle = $titles[$lastPart] ?? ucfirst(str_replace('_', ' ', $lastPart));
33+
34+
foreach ($routes->getRoutes() as $route) {
35+
$routeName = $route->getName();
36+
if (!$routeName) continue;
37+
38+
Breadcrumbs::for($routeName, function (Trail $trail, $params = []) use ($routeName, $parts, $titles) {
39+
foreach ($parts as $i => $segment) {
40+
$paramKey = str_singular($segment);
41+
$hasParam = array_key_exists($paramKey, $params);
42+
43+
$titleKey = $titles[$segment] ?? $segment;
44+
$title = __($titleKey);
45+
$urlParts = array_slice($parts, 0, $i + 1);
46+
$routeGuess = implode('.', $urlParts) . '.index';
47+
48+
$routeParams = [];
49+
foreach ($urlParts as $seg) {
50+
$key = str_singular($seg);
51+
if (array_key_exists($key, $params)) {
52+
$routeParams[$key] = $params[$key];
53+
}
54+
}
55+
56+
$trail->push($title, route($routeGuess, $routeParams));
57+
}
58+
59+
if (str_ends_with($routeName, '.create')) {
60+
$trail->push('Створити', route($routeName, $params));
61+
} elseif (str_ends_with($routeName, '.edit')) {
62+
$trail->push('Редагувати', route($routeName, $params));
63+
} elseif (str_ends_with($routeName, '.show')) {
64+
$trail->push('Деталі', route($routeName, $params));
65+
}
66+
});
67+
}
68+
69+
return $routes;
70+
});
71+
72+

src/Breadcrumbs/Trail.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Step2Dev\LazyBreadcrumb\Breadcrumbs;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class Trail
8+
{
9+
protected array $breadcrumbs = [];
10+
11+
public function push(string $title, string $url): static
12+
{
13+
$this->breadcrumbs[] = compact('title', 'url');
14+
return $this;
15+
}
16+
17+
public function get(): array
18+
{
19+
return $this->breadcrumbs;
20+
}
21+
22+
public function model(Model $model, ?string $url = null): static
23+
{
24+
$title = $model->name ?? $model->title ?? $model->slug ?? '';
25+
$url ??= url()->current();
26+
return $this->push($title, $url);
27+
}
28+
29+
public function replaceLast(string $title): static
30+
{
31+
if (!empty($this->breadcrumbs)) {
32+
$this->breadcrumbs[array_key_last($this->breadcrumbs)]['title'] = $title;
33+
}
34+
return $this;
35+
}
36+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Step2Dev\LazyBreadcrumb\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Facades\Route;
7+
use Step2dev\LazyBreadcrumb\Breadcrumbs;
8+
9+
class ListBreadcrumbsCommand extends Command
10+
{
11+
protected $signature = 'breadcrumbs:list {--missing : Show only routes without breadcrumbs}';
12+
protected $description = 'List all named routes and whether they have breadcrumbs';
13+
14+
public function handle(): void
15+
{
16+
$routes = collect(Route::getRoutes())->filter(fn ($route) => $route->getName());
17+
18+
if ($this->option('missing')) {
19+
$routes = $routes->filter(fn ($route) => !Breadcrumbs::has($route->getName()));
20+
}
21+
22+
$this->table(
23+
['Route name', 'Breadcrumb'],
24+
$routes->map(fn ($route) => [
25+
$route->getName(),
26+
Breadcrumbs::has($route->getName()) ? '✅ defined' : '❌ missing',
27+
])
28+
);
29+
}
30+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Step2Dev\LazyBreadcrumb\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Step2dev\LazyBreadcrumb\Breadcrumbs;
7+
use Step2dev\LazyBreadcrumb\Breadcrumbs\Trail;
8+
9+
class MakeBreadcrumbCommand extends Command
10+
{
11+
protected $signature = 'make:breadcrumb {name : The route name for the breadcrumb}';
12+
protected $description = 'Create a breadcrumb entry in routes/breadcrumbs.php';
13+
14+
public function handle(): void
15+
{
16+
$name = $this->argument('name');
17+
$file = base_path('routes/breadcrumbs.php');
18+
19+
if (!file_exists($file)) {
20+
file_put_contents($file, "<?php\n\nuse Step2dev\\LazyBreadcrumb\\Breadcrumbs;\nuse Step2dev\\LazyBreadcrumb\\Breadcrumbs\\Trail;\n\n");
21+
$this->info('Created routes/breadcrumbs.php');
22+
}
23+
24+
$stub = <<<EOT
25+
26+
Breadcrumbs::for('{$name}', function (Trail \$trail) {
27+
\$trail->push(__('Breadcrumb Title'), route('{$name}'));
28+
});
29+
EOT;
30+
31+
file_put_contents($file, $stub . PHP_EOL, FILE_APPEND);
32+
33+
$this->info("Breadcrumb '{$name}' added to routes/breadcrumbs.php");
34+
}
35+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Step2Dev\LazyBreadcrumb\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Facades\Route;
7+
use Illuminate\Filesystem\Filesystem;
8+
use Step2dev\LazyBreadcrumb\Breadcrumbs;
9+
10+
class SyncBreadcrumbsCommand extends Command
11+
{
12+
protected $signature = 'route:breadcrumbs:sync {--force : Overwrite existing routes/breadcrumbs.php file}';
13+
protected $description = 'Generate breadcrumbs stubs for all named routes not yet defined';
14+
15+
public function handle(): void
16+
{
17+
$fs = new Filesystem();
18+
$path = base_path('routes/breadcrumbs.php');
19+
20+
if (!$fs->exists($path)) {
21+
$fs->put($path, "<?php\n\nuse Step2dev\\LazyBreadcrumb\\Breadcrumbs;\nuse Step2dev\\LazyBreadcrumb\\Breadcrumbs\\Trail;\n");
22+
$this->info('Created routes/breadcrumbs.php');
23+
} elseif (!$this->option('force')) {
24+
$this->info('Breadcrumbs file exists. Use --force to regenerate.');
25+
}
26+
27+
$existing = file_get_contents($path);
28+
$added = 0;
29+
30+
foreach (Route::getRoutes() as $route) {
31+
$name = $route->getName();
32+
if (!$name || Breadcrumbs::has($name) || str_contains($existing, "Breadcrumbs::for('{$name}'")) {
33+
continue;
34+
}
35+
36+
$stub = <<<EOT
37+
38+
Breadcrumbs::for('{$name}', function (Trail \$trail) {
39+
\$trail->push(__('Breadcrumb Title'), route('{$name}'));
40+
});
41+
EOT;
42+
43+
$fs->append($path, $stub . PHP_EOL);
44+
$added++;
45+
}
46+
47+
$this->info("✅ Added {$added} breadcrumb definitions.");
48+
}
49+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Step2dev\LazyBreadcrumb\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Step2dev\LazyBreadcrumb\Breadcrumbs;
7+
8+
class TestBreadcrumbsCommand extends Command
9+
{
10+
protected $signature = 'breadcrumbs:test';
11+
protected $description = 'Test all registered breadcrumb definitions';
12+
13+
public function handle(): void
14+
{
15+
$all = Breadcrumbs::all();
16+
$failures = [];
17+
18+
foreach ($all as $name) {
19+
try {
20+
$items = Breadcrumbs::generate($name);
21+
if (!is_array($items)) {
22+
$failures[] = [$name, 'Returned non-array'];
23+
}
24+
} catch (\Throwable $e) {
25+
$failures[] = [$name, $e->getMessage()];
26+
}
27+
}
28+
29+
if (count($failures)) {
30+
$this->table(['Route', 'Error'], $failures);
31+
$this->error(count($failures) . ' failed.');
32+
} else {
33+
$this->info('✅ All breadcrumbs passed');
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)