├── .gitignore
├── src
├── Http
│ ├── PermalinkController.php
│ └── ResolvesPermalinkView.php
├── Contracts
│ ├── ActionFactory.php
│ ├── PathBuilder.php
│ ├── Permalinkable.php
│ └── SeoBuilder.php
├── Builders
│ ├── BaseBuilder.php
│ ├── TwitterBuilder.php
│ ├── OpenGraphBuilder.php
│ ├── MetaBuilder.php
│ └── Builder.php
├── helpers.php
├── Middleware
│ ├── BuildSeo.php
│ └── ResolvePermalinkEntities.php
├── Routing
│ ├── ReplacesRouter.php
│ ├── Route.php
│ └── Router.php
├── Services
│ ├── ActionFactory.php
│ └── PathBuilder.php
├── Console
│ └── InstallRouter.php
├── EntityObserver.php
├── PermalinkObserver.php
├── PermalinkServiceProvider.php
├── PermalinkManager.php
├── PermalinkSeo.php
├── HasPermalinks.php
└── Permalink.php
├── tests
├── Support
│ ├── Models
│ │ ├── Company.php
│ │ ├── UserWithSoftDeletes.php
│ │ ├── UserWithDisabledPermalinkHandling.php
│ │ ├── User.php
│ │ └── UserWithDefaultSeoAttributes.php
│ ├── Kernel.php
│ ├── factories
│ │ ├── Company.php
│ │ ├── User.php
│ │ ├── UserWithSoftDeletes.php
│ │ ├── UserWithDefaultSeoAttributes.php
│ │ └── UserWithDsiabledPermalinkHandling.php
│ ├── Controllers
│ │ └── TestController.php
│ └── migrations
│ │ ├── 2014_10_12_000000_add_soft_deletes_to_users_table.php
│ │ └── 2018_11_06_095050_create_companies_table.php
├── Unit
│ ├── ArrayUndotTest.php
│ ├── ActionFactoryTest.php
│ └── PathBuilderTest.php
├── Feature
│ ├── Routing
│ │ ├── ResolvingTest.php
│ │ └── RoutingTest.php
│ ├── HasPermalinks
│ │ ├── ReadingTest.php
│ │ ├── NestingTest.php
│ │ ├── DeletingTest.php
│ │ ├── SeoAttributesTest.php
│ │ ├── CreatingTest.php
│ │ ├── ActionTest.php
│ │ └── SlugTest.php
│ └── Permalink
│ │ ├── UpdatePermalinkTest.php
│ │ └── CreatePermalinkTest.php
└── TestCase.php
├── .travis.yml
├── LICENSE
├── phpunit.xml
├── migrations
└── 2018_06_01_092537_create_permalinks_table.php
├── composer.json
├── config
└── permalink.php
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /.idea/
3 | composer.lock
4 | .phpunit.result.cache
5 |
--------------------------------------------------------------------------------
/src/Http/PermalinkController.php:
--------------------------------------------------------------------------------
1 | define(Company::class, function (Faker $faker) {
7 | return [
8 | 'name' => $faker->name,
9 | ];
10 | });
--------------------------------------------------------------------------------
/tests/Support/Models/UserWithSoftDeletes.php:
--------------------------------------------------------------------------------
1 | helper->disableTwitter();
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Builders/OpenGraphBuilder.php:
--------------------------------------------------------------------------------
1 | helper->disableOpenGraph();
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Contracts/PathBuilder.php:
--------------------------------------------------------------------------------
1 | define(User::class, function (Faker $faker) {
7 | return [
8 | 'name' => $faker->name,
9 | 'email' => $faker->email,
10 | 'password' => ''
11 | ];
12 | });
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.3
5 |
6 | cache:
7 | directories:
8 | - $HOME/.composer/cache
9 |
10 | before_install:
11 | - travis_retry composer self-update
12 |
13 | install: travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest
14 |
15 | script: vendor/bin/phpunit --verbose
16 |
--------------------------------------------------------------------------------
/src/helpers.php:
--------------------------------------------------------------------------------
1 | route($permalink);
13 | }
14 | }
--------------------------------------------------------------------------------
/tests/Support/factories/UserWithSoftDeletes.php:
--------------------------------------------------------------------------------
1 | define(UserWithSoftDeletes::class, function (Faker $faker) {
7 | return [
8 | 'name' => $faker->name,
9 | 'email' => $faker->email,
10 | 'password' => ''
11 | ];
12 | });
--------------------------------------------------------------------------------
/tests/Support/factories/UserWithDefaultSeoAttributes.php:
--------------------------------------------------------------------------------
1 | define(UserWithDefaultSeoAttributes::class, function (Faker $faker) {
7 | return [
8 | 'name' => $faker->name,
9 | 'email' => $faker->email,
10 | 'password' => ''
11 | ];
12 | });
--------------------------------------------------------------------------------
/tests/Support/factories/UserWithDsiabledPermalinkHandling.php:
--------------------------------------------------------------------------------
1 | define(UserWithDisabledPermalinkHandling::class, function (Faker $faker) {
7 | return [
8 | 'name' => $faker->name,
9 | 'email' => $faker->email,
10 | 'password' => ''
11 | ];
12 | });
13 |
--------------------------------------------------------------------------------
/src/Contracts/Permalinkable.php:
--------------------------------------------------------------------------------
1 | request($request)->runBuilders();
19 |
20 | return $next($request);
21 | }
22 | }
--------------------------------------------------------------------------------
/tests/Unit/ArrayUndotTest.php:
--------------------------------------------------------------------------------
1 | 1,
15 | 'foo.baz' => 2
16 | ];
17 |
18 | $result = Arr::undot($given);
19 |
20 | $this->assertEquals(['foo' => ['bar' => 1, 'baz' => 2]], $result);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Support/Controllers/TestController.php:
--------------------------------------------------------------------------------
1 | route()->permalink()->seo['title'];
22 | }
23 |
24 | public function typehinted(Permalink $permalink)
25 | {
26 | return $permalink->slug;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Routing/ReplacesRouter.php:
--------------------------------------------------------------------------------
1 | router = $this->app['router'];
15 |
16 | $this->router->replaceMiddleware(
17 | $this->routeMiddleware + $this->router->getMiddleware(),
18 | $this->middlewareGroups + $this->router->getMiddlewareGroups()
19 | );
20 |
21 | return parent::dispatchToRouter();
22 | }
23 | }
--------------------------------------------------------------------------------
/tests/Unit/ActionFactoryTest.php:
--------------------------------------------------------------------------------
1 | 'foo', 'action' => 'welcome']);
16 |
17 | $this->assertEquals('Devio\Permalink\Http\PermalinkController@view', $factory->resolve($permalink));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Support/migrations/2014_10_12_000000_add_soft_deletes_to_users_table.php:
--------------------------------------------------------------------------------
1 | softDeletes();
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | *
24 | * @return void
25 | */
26 | public function down()
27 | {
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Http/ResolvesPermalinkView.php:
--------------------------------------------------------------------------------
1 | getShortName());
23 | $data[$key] = $param;
24 | }
25 |
26 | return view($permalink->rawAction, $data);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Support/migrations/2018_11_06_095050_create_companies_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 |
19 | $table->string('name');
20 |
21 | $table->timestamps();
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | *
28 | * @return void
29 | */
30 | public function down()
31 | {
32 | Schema::dropIfExists('companies');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Support/Models/User.php:
--------------------------------------------------------------------------------
1 | 'entity.name'
35 | ];
36 | }
37 | }
--------------------------------------------------------------------------------
/tests/Support/Models/UserWithDefaultSeoAttributes.php:
--------------------------------------------------------------------------------
1 | route();
19 |
20 | if ($route->hasPermalink()) {
21 | foreach ($route->signatureParameters() as $parameter) {
22 | $type = $parameter->getType();
23 |
24 | if ($type && $type->getName() == Permalink::class) {
25 | $route->parameters[] = $route->permalink();
26 | } else {
27 | $route->parameters[] = $route->permalink()->entity;
28 | }
29 | }
30 | }
31 |
32 | return $next($request);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Builders/MetaBuilder.php:
--------------------------------------------------------------------------------
1 | helper->meta()->getMiscEntity()->add(
15 | 'robots', implode(',', (array) $robots)
16 | );
17 | }
18 |
19 | /**
20 | * Set the canonical URL.
21 | *
22 | * @param $content
23 | */
24 | public function setCanonical($content)
25 | {
26 | $this->helper->meta()->getMiscEntity()->setUrl($content);
27 | }
28 |
29 | /**
30 | * @inheritdoc
31 | */
32 | public function disable(): void
33 | {
34 | // In this case we won't perform any action for the Meta builder so it
35 | // can not be disabled as it would disable the page title. To modify
36 | // this behaviour, feel free to replace the class in the container.
37 | }
38 | }
--------------------------------------------------------------------------------
/tests/Feature/Routing/ResolvingTest.php:
--------------------------------------------------------------------------------
1 | create(['name' => 'israel ortuno']);
14 |
15 | $this->assertEquals('http://localhost/israel-ortuno', permalink($user));
16 | }
17 |
18 | /** @test */
19 | public function it_can_resolve_route_using_integer()
20 | {
21 | factory(User::class)->create(['name' => 'israel ortuno']);
22 |
23 | $this->assertEquals('http://localhost/israel-ortuno', permalink(1));
24 | }
25 |
26 | /** @test */
27 | public function it_can_resolve_route_using_permalink()
28 | {
29 | $user = factory(User::class)->create(['name' => 'israel ortuno']);
30 |
31 | $this->assertEquals('http://localhost/israel-ortuno', permalink($user->permalink));
32 | }
33 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Israel Ortuño
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 | app/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/Services/ActionFactory.php:
--------------------------------------------------------------------------------
1 | rawAction) {
20 | $action = Permalink::getMappedaction($action) ?? $action;
21 | } elseif ($entity = $permalink->getRelationValue('entity')) {
22 | $action = $entity->permalinkAction();
23 | }
24 |
25 | return $this->buildAction($action);
26 | }
27 |
28 | /**
29 | * Resolve the view or controller for the given action.
30 | *
31 | * @param $action
32 | * @return string
33 | */
34 | protected function buildAction($action)
35 | {
36 | return view()->exists($action)
37 | ? PermalinkController::class . '@view'
38 | : $action;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Feature/HasPermalinks/ReadingTest.php:
--------------------------------------------------------------------------------
1 | create(['name' => 'foo']);
15 |
16 | $this->assertEquals('foo', $user->routeSlug);
17 | }
18 |
19 | /** @test */
20 | public function it_returns_null_for_not_found_permalink_slug()
21 | {
22 | $user = factory(User::class)->make(['name' => 'foo']);
23 | $user->disablePermalinkHandling(); // = false;
24 | $user->save();
25 |
26 | $this->assertNull($user->routeSlug);
27 | }
28 |
29 | /** @test */
30 | public function it_can_get_path_from_permalink()
31 | {
32 | Permalink::create(['slug' => 'foo', 'parent_for' => User::class]);
33 | $user = factory(User::class)->create(['name' => 'foo']);
34 |
35 | $this->assertEquals('foo/foo', $user->routePath);
36 | }
37 |
38 | /** @test */
39 | public function it_gets_the_related_permalink_key()
40 | {
41 | $user = factory(User::class)->create(['name' => 'foo']);
42 |
43 | $this->assertEquals(1, $user->permalinkKey);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Unit/PathBuilderTest.php:
--------------------------------------------------------------------------------
1 | '/users',
15 | 'parent_for' => User::class
16 | ]);
17 |
18 | $slugs = PathBuilder::parentPath(User::class);
19 |
20 | $this->assertEquals(['users'], $slugs);
21 | }
22 |
23 | /** @test */
24 | public function it_can_generate_parent_path_with_object_instance()
25 | {
26 | Permalink::create([
27 | 'slug' => '/users',
28 | 'parent_for' => User::class
29 | ]);
30 |
31 | $slugs = PathBuilder::parentPath(new User);
32 |
33 | $this->assertEquals(['users'], $slugs);
34 | }
35 |
36 | /** @test */
37 | public function it_nest_paths_recursively()
38 | {
39 | Permalink::create([
40 | 'slug' => 'account'
41 | ]);
42 |
43 | Permalink::create([
44 | 'slug' => '/users',
45 | 'parent_for' => User::class,
46 | 'parent_id' => 1
47 | ]);
48 |
49 |
50 | $slugs = PathBuilder::parentPath(new User);
51 |
52 | $this->assertEquals(['account', 'users'], $slugs);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/migrations/2018_06_01_092537_create_permalinks_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 |
19 | $table->string('slug');
20 | $table->unsignedInteger('parent_id')->nullable();
21 | $table->string('parent_for')->nullable();
22 |
23 | $table->string('entity_type')->nullable();
24 | $table->unsignedBigInteger('entity_id')->nullable();
25 |
26 | $table->string('action')->nullable();
27 | $table->string('final_path')->nullable();
28 | $table->json('seo')->nullable();
29 |
30 | $table->softDeletes();
31 |
32 | $table->timestamps();
33 |
34 | $table->index(['entity_type', 'entity_id']);
35 | $table->unique(['slug', 'parent_id'], 'UNIQUE_SLUG_AND_PARENT');
36 | $table->unique(['final_path'], 'UNIQUE_FULL_PATH');
37 | $table->unique(['parent_for'], 'UNIQUE_PARENT_FOR');
38 | });
39 | }
40 |
41 | /**
42 | * Reverse the migrations.
43 | *
44 | * @return void
45 | */
46 | public function down()
47 | {
48 | Schema::dropIfExists('permalinks');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "devio/permalink",
3 | "description": "Permalink routing system for Laravel. Advanced database driven routes. Handle your permalinks + SEO parameters directly from database.",
4 | "keywords": [
5 | "permalink",
6 | "laravel",
7 | "routing",
8 | "database routing",
9 | "dynamic routes",
10 | "wordpress"
11 | ],
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "Israel Ortuño",
16 | "email": "ai.ortuno@gmail.com"
17 | }
18 | ],
19 | "require": {
20 | "illuminate/support": "^6.0|^7.0|^8.0",
21 | "illuminate/database": "^6.0|^7.0|^8.0",
22 | "illuminate/routing": "^6.0|^7.0|^8.0",
23 | "cviebrock/eloquent-sluggable": "^6.0|^7.0|^8.0",
24 | "arcanedev/seo-helper": "^2.0|^3.0|^4.0",
25 | "laravel/legacy-factories": "^1.1"
26 | },
27 | "require-dev": {
28 | "orchestra/testbench": "^4.0|^5.0|^6.0",
29 | "phpunit/phpunit": "~8.0",
30 | "mockery/mockery": "^1.1"
31 | },
32 | "autoload": {
33 | "psr-4": {
34 | "Devio\\Permalink\\": "src/"
35 | },
36 | "files": [
37 | "src/helpers.php"
38 | ]
39 | },
40 | "autoload-dev": {
41 | "psr-4": {
42 | "Devio\\Permalink\\Tests\\": "tests"
43 | }
44 | },
45 | "extra": {
46 | "laravel": {
47 | "aliases": {
48 | },
49 | "providers": [
50 | "Devio\\Permalink\\PermalinkServiceProvider"
51 | ]
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Feature/HasPermalinks/NestingTest.php:
--------------------------------------------------------------------------------
1 | create();
16 | $parent = Permalink::create(['slug' => 'foo', 'parent_for' => User::class]);
17 | $child = Permalink::create(['slug' => 'bar', 'entity_type' => User::class, 'entity_id' => 1]);
18 |
19 | $this->assertEquals($parent->getKey(), $child->parent_id);
20 | $this->assertCount(1, $parent->children);
21 | $this->assertEquals($parent->id, $child->parent->id);
22 | }
23 |
24 | /** @test */
25 | public function it_will_have_unique_parent_for_records()
26 | {
27 | $this->expectException(QueryException::class);
28 |
29 | Permalink::create(['slug' => 'foo', 'parent_for' => User::class]);
30 | Permalink::create(['slug' => 'foo', 'parent_for' => User::class]);
31 | }
32 |
33 | /** @test */
34 | public function it_wont_nest_permalink_if_disabled()
35 | {
36 | factory(User::class)->create();
37 | config()->set('permalink.nest_to_parent_on_create', false);
38 |
39 | $parent = Permalink::create(['slug' => 'foo', 'parent_for' => User::class]);
40 | $child = Permalink::create(['slug' => 'bar', 'entity_type' => User::class, 'entity_id' => 1]);
41 |
42 | $this->assertNull($child->parent);
43 | $this->assertCount(0, $parent->children);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Feature/HasPermalinks/DeletingTest.php:
--------------------------------------------------------------------------------
1 | create();
15 | $user->delete();
16 | $this->assertFalse($user->permalink->exists);
17 | }
18 |
19 | /** @test */
20 | public function it_softdeletes_permalink_in_cascade()
21 | {
22 | $user = factory(UserWithSoftDeletes::class)->create();
23 | $user->delete();
24 | $this->assertTrue($user->permalink->trashed());
25 | }
26 |
27 | /** @test */
28 | public function it_forces_permalink_deletion_if_entity_forces_deletion()
29 | {
30 | $user = factory(UserWithSoftDeletes::class)->create();
31 | $user->forceDelete();
32 | $this->assertFalse($user->permalink->exists);
33 | }
34 |
35 | /** @test */
36 | public function it_restores_permalink()
37 | {
38 | $user = factory(UserWithSoftDeletes::class)->create();
39 | $user->delete();
40 | $user->restore();
41 | $this->assertFalse($user->permalink->trashed());
42 | }
43 |
44 | /** @test */
45 | public function it_wont_delete_permalink_if_handling_is_disabled()
46 | {
47 | $user = factory(User::class)->create();
48 | $user->disablePermalinkHandling();
49 | $user->delete();
50 | $this->assertTrue($user->hasPermalink());
51 | $this->assertDatabaseHas('permalinks', ['entity_id' => $user->id]);
52 | }
53 | }
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | loadLaravelMigrations('testing');
25 | $this->loadMigrationsFrom(__DIR__ . '/Support/migrations');
26 | $this->withFactories(__DIR__ . '/Support/factories');
27 | }
28 |
29 | protected function getEnvironmentSetUp($app)
30 | {
31 | $app['config']->set('database.default', 'testing');
32 | $app['config']->set('database.connections.testing', [
33 | 'driver' => 'sqlite',
34 | 'database' => ':memory:',
35 | 'prefix' => '',
36 | ]);
37 | }
38 |
39 | protected function resolveApplicationHttpKernel($app)
40 | {
41 | $app->singleton('Illuminate\Contracts\Http\Kernel', Kernel::class);
42 | }
43 |
44 | protected function getPackageProviders($app)
45 | {
46 | return [
47 | SeoHelperServiceProvider::class,
48 | PermalinkServiceProvider::class,
49 | \Cviebrock\EloquentSluggable\ServiceProvider::class
50 | ];
51 | }
52 |
53 | protected function reloadRoutes()
54 | {
55 | $this->app['router']->loadPermalinks();
56 | }
57 | }
--------------------------------------------------------------------------------
/tests/Feature/Permalink/UpdatePermalinkTest.php:
--------------------------------------------------------------------------------
1 | 'foo', 'action' => TestController::class]);
14 | $permalink2 = Permalink::create(['slug' => 'bar', 'action' => TestController::class]);
15 |
16 | $permalink2->update(['slug' => 'foo']);
17 |
18 | $this->assertEquals('foo-1', $permalink2->slug);
19 | $this->assertDatabaseHas('permalinks', ['slug' => 'foo']);
20 | $this->assertDatabaseHas('permalinks', ['slug' => 'foo-1']);
21 | }
22 |
23 | /** @test */
24 | public function it_will_rebuild_children_path_if_slug_is_updated()
25 | {
26 | $permalink = Permalink::create(['slug' => 'foo', 'action' => TestController::class]);
27 | $permalink2 = Permalink::create(['slug' => 'bar', 'action' => TestController::class, 'parent_id' => $permalink->id]);
28 |
29 | $permalink->update(['slug' => 'baz']);
30 |
31 | $this->assertDatabaseHas('permalinks', ['final_path' => 'baz/bar']);
32 | }
33 |
34 | /** @test */
35 | public function it_wont_rebuild_children_if_disabled()
36 | {
37 | config()->set('permalink.rebuild_children_on_update');
38 |
39 | $permalink = Permalink::create(['slug' => 'foo', 'action' => TestController::class]);
40 | $permalink2 = Permalink::create(['slug' => 'bar', 'action' => TestController::class, 'parent_id' => $permalink->id]);
41 |
42 | $permalink->update(['slug' => 'baz']);
43 |
44 | $this->assertDatabaseHas('permalinks', ['final_path' => 'foo/bar']);
45 | }
46 | }
--------------------------------------------------------------------------------
/tests/Feature/HasPermalinks/SeoAttributesTest.php:
--------------------------------------------------------------------------------
1 | create();
14 |
15 | $this->assertEquals('title', $user->permalink->seo['title']);
16 | $this->assertEquals('description', $user->permalink->seo['description']);
17 | $this->assertEquals('twitter title', $user->permalink->seo['twitter']['title']);
18 | $this->assertEquals('twitter description', $user->permalink->seo['twitter']['description']);
19 | $this->assertEquals('og title', $user->permalink->seo['opengraph']['title']);
20 | $this->assertEquals('og description', $user->permalink->seo['opengraph']['description']);
21 | }
22 |
23 | /** @test */
24 | public function it_overwrites_default_seo_values_if_provided()
25 | {
26 | $user = factory(UserWithDefaultSeoAttributes::class)->create([
27 | 'permalink' => [
28 | 'seo' => ['title' => 'foo', 'description' => 'bar baz']
29 | ]
30 | ]);
31 |
32 | $this->assertEquals('foo', $user->permalink->seo['title']);
33 | $this->assertEquals('bar baz', $user->permalink->seo['description']);
34 | }
35 |
36 | /** @test */
37 | public function it_populates_seo_attributes_only_when_creating()
38 | {
39 | $user = factory(UserWithDefaultSeoAttributes::class)->create([
40 | 'permalink' => [
41 | 'seo' => ['title' => 'foo']
42 | ]
43 | ]);
44 |
45 | $user->update(['name' => 'bar baz']);
46 |
47 | $this->assertEquals('foo', $user->permalink->seo['title']);
48 | }
49 | }
--------------------------------------------------------------------------------
/tests/Feature/Permalink/CreatePermalinkTest.php:
--------------------------------------------------------------------------------
1 | 'foo', 'action' => TestController::class]);
16 | $permalink2 = Permalink::create(['slug' => 'foo', 'action' => TestController::class]);
17 |
18 | $this->assertEquals('foo', $permalink->slug);
19 | $this->assertEquals('foo-1', $permalink2->slug);
20 | $this->assertDatabaseHas('permalinks', ['slug' => 'foo']);
21 | $this->assertDatabaseHas('permalinks', ['slug' => 'foo-1']);
22 | }
23 |
24 | /** @test */
25 | public function it_can_create_a_nested_permalink()
26 | {
27 | $parent = factory(Company::class)->create(['name' => 'foo']);
28 | $user = factory(UserWithDisabledPermalinkHandling::class)
29 | ->create(['name' => 'bar',]);
30 |
31 | $user->createPermalink(['parent_id' => $parent->permalink->id]);
32 |
33 | $this->assertDatabaseHas('permalinks', ['final_path' => 'foo/bar']);
34 | }
35 |
36 | /** @test */
37 | public function it_can_create_nested_permalink_from_manager()
38 | {
39 | $manager = new \Devio\Permalink\PermalinkManager;
40 | $parent = factory(Company::class)->create(['name' => 'foo']);
41 | $user = factory(UserWithDisabledPermalinkHandling::class)
42 | ->create(['name' => 'bar',]);
43 |
44 | $manager->create($user, ['parent_id' => $parent->permalink->id]);
45 |
46 | $this->assertDatabaseHas('permalinks', ['final_path' => 'foo/bar']);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Console/InstallRouter.php:
--------------------------------------------------------------------------------
1 | option('default') == true) {
17 | return $this->inKernel();
18 | }
19 |
20 | $choice = $this->choice('Where do you want to replace the router?', ['Http/Kernel.php (Recommended)', 'bootstrap/app.php (Advanced)'], 0);
21 |
22 | Str::contains($choice, 'bootstrap') ?
23 | $this->inBootstrap() : $this->inKernel();
24 | }
25 |
26 | public function inBootstrap()
27 | {
28 | $app = file_get_contents(base_path('bootstrap/app.php'));
29 |
30 | if (strpos($app, '\Devio\Permalink\Routing\Router::class') !== false) {
31 | $this->info('The router class has already been replaced.');
32 | return;
33 | }
34 |
35 | $from = '/' . preg_quote('$app->singleton', '/') . '/';
36 | $to = '$app->singleton(\'router\', \Devio\Permalink\Routing\Router::class);' . PHP_EOL . PHP_EOL . '$app->singleton';
37 |
38 | file_put_contents(
39 | base_path('bootstrap/app.php'),
40 | preg_replace($from, $to, $app, 1)
41 | );
42 | }
43 |
44 | public function inKernel()
45 | {
46 | $kernel = file_get_contents(app_path('Http/Kernel.php'));
47 |
48 | if (strpos($kernel, 'ReplacesRouter') !== false) {
49 | $this->info('The Kernel is already using the ReplacesRouter trait.');
50 | return;
51 | }
52 |
53 | $from = '/' . preg_quote('{', '/') . '/';
54 | $to = '{' . PHP_EOL . ' use \Devio\Permalink\Routing\ReplacesRouter;' . PHP_EOL;
55 |
56 | file_put_contents(
57 | app_path('Http/Kernel.php'),
58 | preg_replace($from, $to, $kernel, 1)
59 | );
60 | }
61 | }
--------------------------------------------------------------------------------
/config/permalink.php:
--------------------------------------------------------------------------------
1 | [
17 | 'prefix' => '',
18 | 'middleware' => [
19 | 'web',
20 | \Devio\Permalink\Middleware\ResolvePermalinkEntities::class
21 | ]
22 | ],
23 |
24 | /*
25 | |--------------------------------------------------------------------------
26 | | Nesting Options
27 | |--------------------------------------------------------------------------
28 | |
29 | | These options control the nesting automation. By default, if a permalink
30 | | has a parent_for value for a certain model, it'll be automatically set
31 | | as child of that record. Disable to manually control this behaviour.
32 | |
33 | | Also you can decide if the package should take care of the nested slug
34 | | consistency. If you update a parent slug, the package will make sure
35 | | all its nested (recursive) permalinks gets their paths updated to
36 | | match that slug. If you want to control ths behaviour, disable.
37 | | Check the NestingService class to understand how it works.
38 | */
39 |
40 | 'nest_to_parent_on_create' => true,
41 | 'rebuild_children_on_update' => true,
42 |
43 | /*
44 | |--------------------------------------------------------------------------
45 | | Automatically Refresh Routes
46 | |--------------------------------------------------------------------------
47 | |
48 | | The route's collection has to be refreshed when a new permalink is added
49 | | to the router. If you are adding multiple permalinks in a row, you may
50 | | consider to disable this feature to prevent performance issues.
51 | |
52 | | Use Devio\Permalink\Routing\Router::refreshRoutes() to refresh the route look-ups.
53 | |
54 | */
55 |
56 | 'refresh_route_lookups' => true
57 | ];
58 |
--------------------------------------------------------------------------------
/tests/Feature/HasPermalinks/CreatingTest.php:
--------------------------------------------------------------------------------
1 | create();
16 |
17 | $this->assertNotNull($user->permalink);
18 | }
19 |
20 | /** @test */
21 | public function it_wont_create_permalink_when_disabled_by_default()
22 | {
23 | $user = factory(UserWithDisabledPermalinkHandling::class)->create();
24 | $user->save();
25 |
26 | $this->assertNull($user->permalink);
27 | }
28 |
29 | /** @test */
30 | public function it_wont_create_permalink_when_disabled()
31 | {
32 | $user = factory(User::class)->make();
33 | $user->disablePermalinkHandling();
34 | $user->save();
35 |
36 | $this->assertNull($user->permalink);
37 | }
38 |
39 | /** @test */
40 | public function it_will_create_a_permalink_when_key_exists_even_if_disabled()
41 | {
42 | $user = factory(User::class)->make([
43 | 'permalink' => [
44 | 'slug' => 'foo',
45 | 'action' => 'bar'
46 | ]
47 | ]);
48 | $user->disablePermalinkHandling();
49 | $user->save();
50 |
51 | $this->assertNotNull($user->permalink);
52 | }
53 |
54 | /** @test */
55 | public function it_loads_permalink_relation()
56 | {
57 | $user = factory(User::class)->create();
58 |
59 | $this->assertTrue($user->relationLoaded('permalink'));
60 | }
61 |
62 | /** @test */
63 | public function it_can_set_permalink_attributes()
64 | {
65 | $user = factory(User::class)->create(['permalink' => ['slug' => 'foo', 'parent_id' => 1, 'parent_for' => 'user']]);
66 |
67 | $this->assertEquals('foo', $user->permalink->slug);
68 | $this->assertEquals(1, $user->permalink->parent_id);
69 | $this->assertEquals('user', $user->permalink->parent_for);
70 | }
71 |
72 | /** @test */
73 | public function it_supports_morph_map()
74 | {
75 | Relation::morphMap(['user' => User::class]);
76 | $user = factory(User::class)->create();
77 |
78 | $this->assertEquals('user', $user->permalink->entity_type);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/tests/Feature/Routing/RoutingTest.php:
--------------------------------------------------------------------------------
1 | 'foo', 'action' => TestController::class . '@index']);
16 |
17 | $this->get('/foo')
18 | ->assertSuccessful();
19 | }
20 |
21 | /** @test */
22 | public function it_can_access_an_existing_permalink_url()
23 | {
24 | Permalink::create(['slug' => 'foo', 'action' => TestController::class . '@index']);
25 |
26 | $this->get('/foo')
27 | ->assertSuccessful();
28 | }
29 |
30 | /** @test */
31 | public function it_can_return_the_content_from_action()
32 | {
33 | Permalink::create(['slug' => 'foo', 'action' => TestController::class . '@index']);
34 |
35 | $this->get('/foo')
36 | ->assertSee('ok');
37 | }
38 |
39 | /** @test */
40 | public function it_gets_500_code_if_action_cannot_be_resolved()
41 | {
42 | Permalink::create(['slug' => 'baz', 'action' => TestController::class . '@nonexisting']);
43 |
44 | $this->get('/baz')
45 | ->assertStatus(500);
46 | }
47 |
48 | /** @test */
49 | public function it_can_override_permalink_routes()
50 | {
51 | Permalink::create(['slug' => 'overwritten', 'action' => TestController::class . '@index']);
52 | Route::get('overwritten', function () {
53 | return 'overwritten';
54 | });
55 |
56 | $this->get('/overwritten')
57 | ->assertSee('overwritten');
58 | }
59 |
60 | /** @test */
61 | public function it_can_set_a_permalink_when_creating_a_route()
62 | {
63 | Route::get('manual', function () {
64 | return request()->route()->permalink()->seo['title'];
65 | })->setPermalink([
66 | 'seo' => ['title' => 'foo']
67 | ]);
68 |
69 | $this->get('/manual')
70 | ->assertSee('foo');
71 | }
72 |
73 | /** @test */
74 | public function it_injects_the_permalink_instance_if_typehinted_as_parameter()
75 | {
76 | Permalink::create(['slug' => 'typehinted', 'action' => TestController::class . '@typehinted']);
77 |
78 | $this->get('/typehinted')->assertSee('typehinted');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Routing/Route.php:
--------------------------------------------------------------------------------
1 | permalink = $permalink;
26 |
27 | if (! is_null($permalink) && $name = $permalink->name) {
28 | $this->name($name);
29 | }
30 | }
31 |
32 | /**
33 | * Get the permalink instance.
34 | *
35 | * @return Permalink|null
36 | */
37 | public function permalink()
38 | {
39 | if (is_numeric($this->permalink)) {
40 | $this->permalink = Permalink::find($this->permalink);
41 | }
42 |
43 | return $this->permalink;
44 | }
45 |
46 | /**
47 | * Alias for permalink().
48 | *
49 | * @return Permalink
50 | */
51 | public function getPermalink()
52 | {
53 | return $this->permalink();
54 | }
55 |
56 | /**
57 | * Set the permalink instance.
58 | *
59 | * @param Permalink $permalink
60 | */
61 | public function setPermalink($permalink)
62 | {
63 | if (! $permalink instanceof Permalink) {
64 | $permalink = $this->buildPermalink($permalink);
65 | }
66 |
67 | $this->permalink = $permalink;
68 | }
69 |
70 | /**
71 | * Check if the current route has a permalink instance attached.
72 | *
73 | * @return bool
74 | */
75 | public function hasPermalink(): bool
76 | {
77 | return (bool) $this->permalink;
78 | }
79 |
80 | /**
81 | * Get a new permalink instance.
82 | *
83 | * @param array $permalink
84 | * @return Permalink
85 | */
86 | public function buildPermalink(array $permalink)
87 | {
88 | return new Permalink($permalink);
89 | }
90 |
91 | /**
92 | * {@inheritdoc}
93 | */
94 | public function prepareForSerialization()
95 | {
96 | parent::prepareForSerialization();
97 |
98 | // We will replace the permalink instance for its key when serializing
99 | // so we won't store the entire Permalink object which would result
100 | // into a large compiled routes file and lack of memory issues.
101 | $this->permalink = $this->permalink()->getKey();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/Feature/HasPermalinks/ActionTest.php:
--------------------------------------------------------------------------------
1 | create();
16 |
17 | $this->assertEquals($user->permalinkAction(), $user->permalink->action);
18 | }
19 |
20 | /** @test */
21 | public function it_can_map_actions()
22 | {
23 | $action = TestController::class . '@index';
24 | Permalink::actionMap(['user.index' => $action]);
25 | $permalink = Permalink::create(['slug' => 'foo', 'action' => $action]);
26 |
27 | $this->assertEquals($action, $permalink->action);
28 | }
29 |
30 | /** @test */
31 | public function it_reads_raw_actions()
32 | {
33 | Permalink::actionMap(['user.index' => TestController::class . '@index']);
34 | $permalink = Permalink::create(['slug' => 'foo', 'action' => 'user.index']);
35 |
36 | $this->assertEquals('user.index', $permalink->rawAction);
37 | }
38 |
39 | /** @test */
40 | public function it_maps_actions_before_saving()
41 | {
42 | Permalink::actionMap(['user.index' => TestController::class . '@index']);
43 | $permalink = Permalink::create(['slug' => 'foo', 'action' => TestController::class . '@index']);
44 |
45 | $this->assertEquals('user.index', $permalink->rawAction);
46 | }
47 |
48 | /** @test */
49 | public function it_can_override_default_actions()
50 | {
51 | $user = factory(User::class)->create([
52 | 'permalink' => [
53 | 'slug' => 'foo',
54 | 'action' => \Devio\Permalink\Tests\Support\Controllers\TestController::class . '@override'
55 | ]
56 | ]);
57 |
58 | $this->get('foo')->assertSee('override');
59 | }
60 |
61 | /** @test */
62 | public function it_supports_view_paths_as_actions()
63 | {
64 | Permalink::create(['slug' => 'foo', 'action' => 'welcome']);
65 |
66 | $this->get('foo')->assertViewIs('welcome');
67 | }
68 |
69 | /** @test */
70 | public function it_will_pass_the_entity_to_the_view()
71 | {
72 | $user = factory(User::class)->create([
73 | 'permalink' => [
74 | 'slug' => 'foo',
75 | 'action' => 'welcome'
76 | ]
77 | ]);
78 |
79 | $response = $this->get('foo');
80 | $response->assertViewHas('user');
81 |
82 | $this->assertEquals($user->getKey(), $response->viewData('user')->getKey());
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/EntityObserver.php:
--------------------------------------------------------------------------------
1 | slugService = $slugService;
22 | }
23 |
24 | public function saved(Model $model)
25 | {
26 | if (! $model->permalinkHandling() && is_null($model->getPermalinkAttributes())) {
27 | return;
28 | }
29 |
30 | $attributes = $this->gatherAttributes($model->getPermalinkAttributes());
31 |
32 | // By checking if 'deleted_at' column has been modified, we can prevent
33 | // re-creating the permalink when the model has been restored because
34 | // this event is fired again and wasRecentlyCreated will be true.
35 | $softDeletingAction = method_exists($model, 'getDeletedAtColumn') && $model->isDirty($model->getDeletedAtColumn());
36 |
37 | ($model->wasRecentlyCreated && ! $softDeletingAction) ?
38 | $model->createPermalink($attributes) : $model->updatePermalink($attributes);
39 | }
40 |
41 | /**
42 | * Restored model event handler.
43 | *
44 | * @param $model
45 | */
46 | public function restored($model)
47 | {
48 | if (! $model->permalinkHandling() || ! $model->hasPermalink()) {
49 | return;
50 | }
51 |
52 | $model->permalink->restore();
53 | }
54 |
55 | /**
56 | * Deleted model event handler.
57 | *
58 | * @param Model $model
59 | */
60 | public function deleted(Model $model)
61 | {
62 | if (! $model->hasPermalink() || ! $model->permalinkHandling()) {
63 | return;
64 | }
65 |
66 | $method = 'forceDelete';
67 |
68 | // The Permalink record should be removed if the main entity has been
69 | // deleted. Here we will check if we should perform a hard or soft
70 | // deletion depending on what is being used ont he main entity.
71 | if (method_exists($model, 'trashed') && ! $model->isForceDeleting()) {
72 | $method = 'delete';
73 | }
74 |
75 | $model->permalink->{$method}();
76 | }
77 |
78 | /**
79 | * Get the attributes from the request or 'permalink' key.
80 | *
81 | * @param null $attributes
82 | * @return array
83 | */
84 | protected function gatherAttributes($attributes = null)
85 | {
86 | $attributes = $attributes ?: request();
87 |
88 | return ($attributes instanceof Request ? $attributes->get('permalink') : $attributes) ?? [];
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/PermalinkObserver.php:
--------------------------------------------------------------------------------
1 | path = $path;
33 | $this->slugService = $slugService;
34 | }
35 |
36 | /**
37 | * Creating event.
38 | *
39 | * @param $model
40 | */
41 | public function creating($permalink)
42 | {
43 | $this->nestToParent($permalink);
44 |
45 | if ($permalink->isDirty('slug') && ! empty($permalink->slug)) {
46 | $this->ensureSlugIsUnique($permalink);
47 | } else {
48 | $this->slugService->slug($permalink);
49 | }
50 |
51 | $this->path->build($permalink);
52 | }
53 |
54 | /**
55 | * Updating event.
56 | *
57 | * @param $model
58 | */
59 | public function updating($permalink)
60 | {
61 | if ($permalink->getOriginal('slug') !== $permalink->slug) {
62 | $this->ensureSlugIsUnique($permalink);
63 |
64 | config('permalink.rebuild_children_on_update')
65 | ? $this->path->recursive($permalink)
66 | : $this->path->single($permalink);
67 | }
68 | }
69 |
70 | /**
71 | * Creates an unique slug for the permalink.
72 | *
73 | * @param $model
74 | */
75 | protected function ensureSlugIsUnique($permalink)
76 | {
77 | if (! $permalink->isDirty('slug') || empty($permalink->slug)) {
78 | return;
79 | }
80 |
81 | // If the user has provided an slug manually, we have to make sure
82 | // that that slug is unique. If it is not, the SlugService class
83 | // will append an incremental suffix to ensure its uniqueness.
84 | $permalink->slug = SlugService::createSlug($permalink, 'slug', $permalink->slug, []);
85 | }
86 |
87 | /**
88 | * Nest the permalink to a parent if found.
89 | *
90 | * @param $model
91 | */
92 | protected function nestToParent($permalink)
93 | {
94 | $entity = $permalink->entity;
95 |
96 | if (! $entity || ! $entity->permalinkNestToParentOnCreate()) {
97 | return;
98 | }
99 |
100 | $parent = PathBuilder::parentFor($entity);
101 |
102 | if (! $permalink->exists && $entity && $parent) {
103 | $permalink->parent_id = $parent->getKey();
104 | $permalink->setRelation('parent', $parent);
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/PermalinkServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(__DIR__ . '/../config/permalink.php', 'permalink');
20 |
21 | $this->publishes([__DIR__ . '/../config/permalink.php' => config_path('permalink.php')], 'permalink-config');
22 |
23 | $this->loadMigrationsFrom(__DIR__ . '/../migrations');
24 |
25 | $this->defineMacros();
26 | }
27 |
28 | /**
29 | * Create the permalink macro.
30 | */
31 | protected function defineMacros()
32 | {
33 | \Illuminate\Routing\Router::macro('replaceMiddleware', function ($middleware = [], $middlewareGroups = []) {
34 | $this->middleware = $middleware;
35 | $this->middlewareGroups = $middlewareGroups;
36 | });
37 |
38 | \Illuminate\Http\Request::macro('permalink', function() {
39 | return method_exists($this->route(), 'permalink') ? $this->route()->permalink() : null;
40 | });
41 |
42 | Arr::macro('undot', function (array $dotArray) {
43 | $array = [];
44 | foreach ($dotArray as $key => $value) {
45 | Arr::set($array, $key, $value);
46 | }
47 |
48 | return $array;
49 | });
50 | }
51 |
52 | /**
53 | * Register the service provider.
54 | */
55 | public function register()
56 | {
57 | $builders = [
58 | 'base' => Builders\BaseBuilder::class,
59 | 'meta' => Builders\MetaBuilder::class,
60 | 'opengraph' => Builders\OpenGraphBuilder::class,
61 | 'twitter' => Builders\TwitterBuilder::class,
62 | ];
63 |
64 | foreach ($builders as $alias => $builder) {
65 | $this->app->singleton("permalink.$alias", function ($app, $parameters) use ($builder) {
66 | $helper = $app->make(SeoHelper::class);
67 |
68 | return (new $builder($helper))
69 | ->permalink($parameters[0])
70 | ->data($parameters[1]);
71 | });
72 | }
73 |
74 | $this->app->singleton('router', Router::class);
75 |
76 | $this->app->singleton(PermalinkSeo::class, function () {
77 | return new PermalinkSeo($this->app['request'], $this->app);
78 | });
79 |
80 | $this->app->singleton(PermalinkManager::class, function () {
81 | return new PermalinkManager;
82 | });
83 |
84 | $this->app->singleton(PathBuilder::class, function() {
85 | return new Services\PathBuilder;
86 | });
87 |
88 | $this->app->singleton(ActionFactory::class, function () {
89 | return new Services\ActionFactory;
90 | });
91 |
92 | $this->commands([
93 | Console\InstallRouter::class,
94 | ]);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Services/PathBuilder.php:
--------------------------------------------------------------------------------
1 | exists) {
15 | return $this->single($model);
16 | } elseif ($model->isDirty('slug')) {
17 | return $this->recursive($model);
18 | }
19 | }
20 |
21 | public function single($model)
22 | {
23 | $model->final_path = $this->getFullyQualifiedPath($model);
24 | }
25 |
26 | public function all()
27 | {
28 | $permalinks = Permalink::withTrashed()->get();
29 |
30 | $permalinks->each(function ($permalink) {
31 | $this->single($permalink);
32 | $permalink->save();
33 | });
34 | }
35 |
36 | public function recursive($model)
37 | {
38 | $path = $model->isDirty('slug') ?
39 | $model->getOriginal('final_path') : $model->final_path;
40 |
41 | $nested = Permalink::withTrashed()
42 | ->where('final_path', 'LIKE', $path . '/%')
43 | ->get();
44 |
45 | $this->single($model);
46 |
47 | $nested->each(function ($permalink) use ($model) {
48 | $permalink->final_path = $model->final_path . '/' . $permalink->slug;
49 | $permalink->save();
50 | });
51 | }
52 |
53 | /**
54 | * @param $model
55 | * @return string
56 | */
57 | public function getFullyQualifiedPath($model)
58 | {
59 | $path = ($model->isNested() && $model->parent) ? $model->parent->final_path : '';
60 |
61 | return trim($path . '/' . $model->slug, '/');
62 | }
63 |
64 | /**
65 | * Find the parent for the given model.
66 | *
67 | * @param $model
68 | * @return mixed
69 | */
70 | public static function parentFor($model)
71 | {
72 | if (is_null($model) || (! is_object($model) && ! class_exists($model))) {
73 | return null;
74 | }
75 |
76 | if (! is_object($model)) {
77 | $model = new $model;
78 | }
79 |
80 | $model = $model->getMorphClass();
81 |
82 | return Permalink::where('parent_for', $model)->first();
83 | }
84 |
85 | /**
86 | * Get the parent route path.
87 | *
88 | * @param $model
89 | * @return array
90 | */
91 | public static function parentPath($model)
92 | {
93 | if ($model instanceof Permalink) {
94 | $model = $model->entity;
95 | }
96 |
97 | $slugs = [];
98 |
99 | $callable = function ($permalink) use (&$callable, &$slugs) {
100 | if (is_null($permalink)) {
101 | return;
102 | }
103 |
104 | array_push($slugs, $permalink->slug);
105 |
106 | if ($permalink->parent) {
107 | $callable($permalink->parent);
108 | }
109 | };
110 |
111 | $callable(static::parentFor($model));
112 |
113 | return array_reverse($slugs);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/PermalinkManager.php:
--------------------------------------------------------------------------------
1 | permalink;
23 | }
24 |
25 | return $item instanceof Permalink ? url($item->final_path) : '#';
26 | }
27 |
28 | /**
29 | * Create a permalink for the given entity.
30 | *
31 | * @param $entity
32 | * @param array $attributes
33 | * @return mixed
34 | */
35 | public function create($entity, $attributes = [])
36 | {
37 | $permalink = $entity->permalink()
38 | ->newRelatedInstanceFor($entity)
39 | // ->setRelation('entity', $entity)
40 | ->fill($this->prepareDefaultSeoAttributes($entity, $attributes));
41 |
42 | $permalink->save();
43 |
44 | return $entity->setRelation('permalink', $permalink);
45 | }
46 |
47 | /**
48 | * Update an existing permalink.
49 | *
50 | * @param $entity
51 | * @param array $attributes
52 | * @return mixed
53 | */
54 | public function update($entity, $attributes = [])
55 | {
56 | if (! $entity->hasPermalink()) {
57 | return $entity;
58 | }
59 |
60 | return $entity->permalink->update($attributes);
61 | }
62 |
63 | /**
64 | * Prepare the default SEO attributes.
65 | *
66 | * @param $entity
67 | * @param $attributes
68 | * @return array
69 | */
70 | protected function prepareDefaultSeoAttributes($entity, $attributes)
71 | {
72 | $attributes = Arr::undot($attributes);
73 | $values = Arr::dot($this->getSeoAttributesArray());
74 |
75 | foreach ($values as $key => $value) {
76 | // We will generate the permalinkSeo* methods in order to populate the
77 | // premalink default SEO data when creating a new permalink record.
78 | // They will be overwritten if any seo data has been provided.
79 | $attribute = 'permalink' . Str::studly(str_replace('.', ' ', $key));
80 |
81 | if (! Arr::get($attributes, $key) && $value = $entity->getAttribute($attribute)) {
82 | Arr::set($attributes, $key, $value);
83 | }
84 | }
85 |
86 | return $attributes;
87 | }
88 |
89 | /**
90 | * Get a value empty seo column structure.
91 | *
92 | * @return array
93 | */
94 | protected function getSeoAttributesArray()
95 | {
96 | return [
97 | 'seo' => [
98 | 'title' => null,
99 | 'description' => null,
100 | 'twitter' => ['title' => null, 'description' => null],
101 | 'opengraph' => ['title' => null, 'description' => null],
102 | ],
103 | ];
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/tests/Feature/HasPermalinks/SlugTest.php:
--------------------------------------------------------------------------------
1 | create(['name' => 'Israel Ortuño']);
16 |
17 | $this->assertEquals('israel-ortuno', $user->permalink->slug);
18 | $this->assertDatabaseHas('permalinks', ['slug' => 'israel-ortuno']);
19 | }
20 |
21 | /** @test */
22 | public function it_creates_unique_slugs()
23 | {
24 | $permalink1 = Permalink::create(['slug' => 'foo']);
25 | $permalink2 = Permalink::create(['slug' => 'foo']);
26 |
27 | $this->assertEquals('foo', $permalink1->slug);
28 | $this->assertEquals('foo-1', $permalink2->slug);
29 | }
30 |
31 | /** @test */
32 | public function it_creates_unique_slugs_from_resource()
33 | {
34 | factory(User::class)->times(2)->create(['name' => 'foo']);
35 |
36 | $this->assertDatabaseHas('permalinks', ['slug' => 'foo']);
37 | $this->assertDatabaseHas('permalinks', ['slug' => 'foo-1']);
38 | }
39 |
40 | /** @test */
41 | public function it_creates_same_slug_if_parent_is_different()
42 | {
43 | $permalink1 = Permalink::create(['slug' => 'foo']);
44 | $permalink2 = Permalink::create(['slug' => 'foo', 'parent_id' => 1]);
45 |
46 | $this->assertEquals('foo', $permalink1->slug);
47 | $this->assertEquals('foo', $permalink2->slug);
48 | }
49 |
50 | /** @test */
51 | public function it_creates_same_slug_for_different_parents_from_resource()
52 | {
53 | Permalink::create(['slug' => 'user', 'parent_for' => User::class]);
54 | Permalink::create(['slug' => 'company', 'parent_for' => Company::class]);
55 |
56 | $user = factory(User::class)->create(['name' => 'foo']);
57 | $company = factory(Company::class)->create(['name' => 'foo']);
58 |
59 | $this->assertEquals('foo', $user->permalink->slug);
60 | $this->assertEquals('foo', $company->permalink->slug);
61 | }
62 |
63 | /** @test */
64 | public function it_creates_unique_slug_when_passing_permalink_array()
65 | {
66 | Permalink::create(['slug' => 'foo']);
67 | $user = factory(User::class)->create(['permalink' => ['slug' => 'foo']]);
68 |
69 | $this->assertEquals('foo-1', $user->permalink->slug);
70 | }
71 |
72 | /** @test */
73 | public function it_allows_same_slug_per_parent()
74 | {
75 | Permalink::create(['slug' => 'foo', 'parent_for' => User::class]);
76 | $user = factory(User::class)->create(['name' => 'foo']);
77 |
78 | $this->assertEquals('foo', $user->permalink->slug);
79 | }
80 |
81 | /** @test */
82 | public function it_makes_slug_attribute_mandatory()
83 | {
84 | // Slug is mandatory as there's only one '' permalink => homepage
85 | $user = factory(User::class)->create(['name' => 'foo', 'permalink' => ['slug' => null]]);
86 | $user2 = factory(User::class)->create(['name' => 'bar', 'permalink' => ['slug' => '']]);
87 |
88 | $this->assertEquals('foo', $user->permalink->slug);
89 | $this->assertEquals('bar', $user2->permalink->slug);
90 | }
91 | }
--------------------------------------------------------------------------------
/src/PermalinkSeo.php:
--------------------------------------------------------------------------------
1 | request = $request;
44 | $this->container = $container;
45 | }
46 |
47 | /**
48 | * @inheritdoc
49 | */
50 | public function runBuilders()
51 | {
52 | if (is_null($permalink = $this->getCurrentPermalink())) {
53 | return;
54 | }
55 |
56 | $builders = $this->getBuildersCollection($permalink);
57 |
58 | foreach ($builders as $builder => $data) {
59 | if ($this->getContainer()->has($binding = 'permalink.' . $builder)) {
60 | $this->getContainer()->make($binding, [$permalink, $data])->build();
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * Prepare the builders array from the permalink SEO data.
67 | *
68 | * @param $seo
69 | * @return array
70 | */
71 | protected function getBuildersCollection($permalink)
72 | {
73 | $seo = Arr::wrap($permalink->seo);
74 |
75 | if (count($base = Arr::except($seo, static::builders))) {
76 | $seo['base'] = $base;
77 | }
78 |
79 | return collect(static::builders)->mapWithKeys(function ($builder) use ($seo) {
80 | return [$builder => Arr::get($seo, $builder)];
81 | });
82 | }
83 |
84 | /**
85 | * Get the current route permalink if any.
86 | *
87 | * @return Permalink
88 | */
89 | protected function getCurrentPermalink()
90 | {
91 | $route = $this->request->route();
92 |
93 | if ($route instanceof Route && ($permalink = $route->permalink()) instanceof Permalink) {
94 | return $permalink;
95 | }
96 |
97 | if ($seo = $this->staticPermalinks[$route->getName()] ?? false) {
98 | return (new Permalink)->fill(compact('seo'));
99 | }
100 |
101 | return null;
102 | }
103 |
104 | /**
105 | * Set the permalink static collection.
106 | *
107 | * @param $permalinks
108 | * @return $this
109 | */
110 | public function permalinks($permalinks)
111 | {
112 | $this->staticPermalinks = $permalinks;
113 |
114 | return $this;
115 | }
116 |
117 | /**
118 | * Set the request instance.
119 | *
120 | * @param $request
121 | * @return $this
122 | */
123 | public function request($request)
124 | {
125 | $this->request = $request;
126 |
127 | return $this;
128 | }
129 |
130 | /**
131 | * Get the container instance.
132 | *
133 | * @return Container
134 | */
135 | public function getContainer()
136 | {
137 | return $this->container;
138 | }
139 |
140 | /**
141 | * Set the container instance.
142 | *
143 | * @param $container
144 | */
145 | public function setContainer($container)
146 | {
147 | $this->container = $container;
148 | }
149 | }
--------------------------------------------------------------------------------
/src/HasPermalinks.php:
--------------------------------------------------------------------------------
1 | permalinkAttributes = $value;
40 | }
41 |
42 | /**
43 | * Get the permalink attributes if any.
44 | *
45 | * @return null
46 | */
47 | public function getPermalinkAttributes()
48 | {
49 | return $this->permalinkAttributes;
50 | }
51 |
52 | /**
53 | * Relation to the permalinks table.
54 | *
55 | * @return mixed
56 | */
57 | public function permalink()
58 | {
59 | return $this->morphOne(Permalink::class, 'entity')->withTrashed();
60 | }
61 |
62 | /**
63 | * Create the permalink for the current entity.
64 | *
65 | * @param $attributes
66 | * @return $this
67 | */
68 | public function createPermalink($attributes = [])
69 | {
70 | return app(PermalinkManager::class)->create($this, $attributes);
71 | }
72 |
73 | /**
74 | * Update the permalink for the current entity.
75 | *
76 | * @param $attributes
77 | * @return $this
78 | */
79 | public function updatePermalink($attributes = [])
80 | {
81 | return app(PermalinkManager::class)->update($this, $attributes);
82 | }
83 |
84 | /**
85 | * Alias to get the existihg permalink ID.
86 | *
87 | * @return |null
88 | */
89 | public function getPermalinkKeyAttribute()
90 | {
91 | return $this->hasPermalink() ? $this->permalink->getKey() : null;
92 | }
93 |
94 | /**
95 | * Resolve the full permalink route.
96 | *
97 | * @return string
98 | */
99 | public function getRouteAttribute()
100 | {
101 | return ($this->exists && $this->hasPermalink()) ?
102 | url($this->permalink->final_path) : null;
103 | }
104 |
105 | /**
106 | * Get the entity slug.
107 | *
108 | * @return null
109 | */
110 | public function getRouteSlugAttribute()
111 | {
112 | return $this->hasPermalink() ? $this->permalink->slug : null;
113 | }
114 |
115 | /**
116 | * Get the permalink nested path.
117 | *
118 | * @return mixed
119 | */
120 | public function getRoutePathAttribute()
121 | {
122 | return $this->hasPermalink() ? trim(parse_url($this->route)['path'], '/') : null;
123 | }
124 |
125 | /**
126 | * Check if the page has a permalink relation.
127 | *
128 | * @return bool
129 | */
130 | public function hasPermalink()
131 | {
132 | return (bool) ! is_null($this->getRelationValue('permalink'));
133 | }
134 |
135 | /**
136 | * Determine if this permalink should be nested to its parent when created.
137 | *
138 | * @return bool
139 | */
140 | public function permalinkNestToParentOnCreate()
141 | {
142 | return config('permalink.nest_to_parent_on_create');
143 | }
144 |
145 | /**
146 | * Determine if automatic permalink handling should be done.
147 | *
148 | * @return bool
149 | */
150 | public function permalinkHandling()
151 | {
152 | return $this->permalinkHandling;
153 | }
154 |
155 | /**
156 | * Enable automatic permalink handling.
157 | *
158 | * @return $this
159 | */
160 | public function enablePermalinkHandling()
161 | {
162 | $this->permalinkHandling = true;
163 |
164 | return $this;
165 | }
166 |
167 | /**
168 | * Disable automatic permalink handling.
169 | *
170 | * @return $this
171 | */
172 | public function disablePermalinkHandling()
173 | {
174 | $this->permalinkHandling = false;
175 |
176 | return $this;
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/Routing/Router.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->group(config('permalink.group'), function () use ($permalinks) {
22 | $this->addPermalinks($permalinks);
23 | });
24 | }
25 |
26 | /**
27 | * @param \Illuminate\Http\Request $request
28 | * @return \Illuminate\Routing\Route|void
29 | */
30 | public function findRoute($request)
31 | {
32 | // First we'll try to find any code defined route for the current request.
33 | // If no route was found, we can then attempt to find if the URL path
34 | // matches a existing permalink. If not just rise up the exception.
35 | try {
36 | return parent::findRoute($request);
37 | } catch (HttpException $e) {
38 | $this->findPermalink($request);
39 | }
40 |
41 | return parent::findRoute($request);
42 | }
43 |
44 | /**
45 | * Get the permalink for the current request if any.
46 | */
47 | public function findPermalink($request)
48 | {
49 | $path = trim($request->getPathInfo(), '/');
50 |
51 | if (! $permalink = Permalink::where('final_path', $path)->first()) {
52 | throw new NotFoundHttpException;
53 | }
54 |
55 | $this->group(config('permalink.group'), function () use ($permalink) {
56 | $this->addPermalinks($permalink);
57 | });
58 | }
59 |
60 | protected function createPermalinkRoute($permalink)
61 | {
62 | $route = $this->newPermalinkRoute($permalink);
63 |
64 | // If we have groups that need to be merged, we will merge them now after this
65 | // route has already been created and is ready to go. After we're done with
66 | // the merge we will be ready to return the route back out to the caller.
67 | if ($this->hasGroupStack()) {
68 | $this->mergeGroupAttributesIntoRoute($route);
69 | }
70 |
71 | $this->addWhereClausesToRoute($route);
72 |
73 | return $route;
74 | }
75 |
76 | /**
77 | * Create a new Route for the given permalink.
78 | *
79 | * @param $permalink
80 | * @return Route
81 | */
82 | protected function newPermalinkRoute($permalink)
83 | {
84 | $path = $this->prefix($permalink->final_path);
85 | $action = $this->convertToControllerAction($permalink->action);
86 |
87 | return tap($this->newRoute($permalink->method, $path, $action), function ($route) use ($permalink) {
88 | $route->setPermalink($permalink);
89 | });
90 | }
91 |
92 | /**
93 | * Add a collection of permalinks to the router.
94 | *
95 | * @param array $permalinks
96 | * @param bool $forceRefresh
97 | * @return Router
98 | */
99 | public function addPermalinks($permalinks = [], $forceRefresh = false)
100 | {
101 | if (! $permalinks instanceof Collection) {
102 | $permalinks = Arr::wrap($permalinks);
103 | }
104 |
105 | foreach ($permalinks as $permalink) {
106 | $this->addPermalink($permalink);
107 | }
108 |
109 | if ($forceRefresh || config('permalink.refresh_route_lookups')) {
110 | $this->refreshRouteLookups();
111 | }
112 |
113 | return $this;
114 | }
115 |
116 | /**
117 | * Add a single permalink to the router.
118 | *
119 | * @param $permalink
120 | * @return Router
121 | */
122 | protected function addPermalink($permalink)
123 | {
124 | if ($permalink->action) {
125 | $route = $this->createPermalinkRoute($permalink);
126 |
127 | $this->routes->add($route);
128 | }
129 |
130 | return $this;
131 | }
132 |
133 | /**
134 | * Create a new route.
135 | *
136 | * @param array|string $methods
137 | * @param string $uri
138 | * @param mixed $action
139 | * @return Route
140 | */
141 | public function newRoute($methods, $uri, $action)
142 | {
143 | return (new Route($methods, $uri, $action))
144 | ->setRouter($this)
145 | ->setContainer($this->container);
146 | }
147 |
148 | /**
149 | * Refresh the route name and action lookups.
150 | */
151 | public function refreshRouteLookups()
152 | {
153 | $this->getRoutes()->refreshNameLookups();
154 | $this->getRoutes()->refreshActionLookups();
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/Builders/Builder.php:
--------------------------------------------------------------------------------
1 | helper = $helper;
42 |
43 | $this->permalink($permalink)->data($data);
44 | }
45 |
46 | /**
47 | * Tasks before the builder has run.
48 | */
49 | protected function before(): void
50 | {
51 | }
52 |
53 | /**
54 | * Tasks after the buidler has run.
55 | */
56 | protected function after(): void
57 | {
58 | }
59 |
60 | /**
61 | * Translate the current instance from database to the SEO helper.
62 | */
63 | public function build(): void
64 | {
65 | // If the data is false, will mean that the object we are suppose to build
66 | // has a value of "false" and will be therefore disabled. This way we is
67 | // possible to control whether we want Twitter or OpenGraph meta tags.
68 | if ($this->data === false) {
69 | $this->disable();
70 | return;
71 | }
72 |
73 | $this->before();
74 |
75 | foreach ($this->data as $key => $content) {
76 | $this->call($key, $content);
77 | }
78 |
79 | $this->after();
80 | }
81 |
82 | protected function call($name, $content)
83 | {
84 | // We will make sure we always provide an array as parameter to the
85 | // builder methods. This way we could pass multiple parameters to
86 | // functions like setTitle and addWebmaster. Flexibility on top!
87 | $content = Arr::wrap($content);
88 |
89 | $builder = $this->getBuilderName();
90 |
91 | // Then we will check if there is a method with that name in this
92 | // class. If so, we'll use it as it may contain any extra logic
93 | // like compiling the content or doing some transformations.
94 | if ($method = $this->methodExists($this, $name)) {
95 | call_user_func_array([$this, $method], $content);
96 | }
97 |
98 | // If there is a matching method into the base SEO helper, we will
99 | // pass the data right to it. This is specially useful to avoid
100 | // specifying a title for every builder (meta, og & twitter).
101 | elseif ($method = $this->methodExists($this->helper, $name)) {
102 | call_user_func_array([$this->helper, $method], $content);
103 | }
104 |
105 | // If the key matches a method in the SEO helper we will just pass
106 | // the content as parameter. This gives a lot of flexibility as
107 | // it allows to manage the package directly from database.
108 | elseif (method_exists($this->helper, $builder)
109 | && $method = $this->methodExists($target = $this->helper->$builder(), $name)) {
110 | call_user_func_array([$target, $method], $content);
111 | }
112 | }
113 |
114 | protected function getBuilderName()
115 | {
116 | $class = (new \ReflectionClass(static::class))->getShortName();
117 |
118 | return lcfirst(str_replace('Builder', '', $class));
119 | }
120 |
121 | /**
122 | * Check if the method exists into the given object.
123 | *
124 | * @param $object
125 | * @param $name
126 | * @return bool|mixed
127 | */
128 | protected function methodExists($object, $name)
129 | {
130 | $name = Str::studly($name);
131 | $methods = ["set{$name}", "add{$name}"];
132 |
133 | foreach ($methods as $method) {
134 | if (method_exists($object, $method)) {
135 | return $method;
136 | }
137 | }
138 |
139 | return false;
140 | }
141 |
142 | /**
143 | * Set the builder Permalink.
144 | *
145 | * @param Permalink $permalink
146 | * @return $this
147 | */
148 | public function permalink(Permalink $permalink = null)
149 | {
150 | $this->permalink = $permalink;
151 |
152 | return $this;
153 | }
154 |
155 | /**
156 | * Set the builder data.
157 | *
158 | * @param array $data
159 | */
160 | public function data($data = [])
161 | {
162 | $this->data = $data = array_filter(Arr::wrap($data), function ($item) {
163 | return ! is_null($item);
164 | });
165 |
166 | return $this;
167 | }
168 |
169 | /**
170 | * @inheritdoc
171 | */
172 | abstract public function disable(): void;
173 | }
174 |
--------------------------------------------------------------------------------
/src/Permalink.php:
--------------------------------------------------------------------------------
1 | 'json'
31 | ];
32 |
33 | /**
34 | * Array to map action class paths to their alias names in database.
35 | *
36 | * @var
37 | */
38 | public static $actionMap = [];
39 |
40 | /**
41 | * Parents cache.
42 | *
43 | * @var array
44 | */
45 | public static $parents = [];
46 |
47 | /**
48 | * Booting the model.
49 | */
50 | public static function boot()
51 | {
52 | parent::boot();
53 | parent::flushEventListeners();
54 |
55 | // Since we want to allow the user to specify an slug rather to always
56 | // generate it automatically, we've to remove the observers added by
57 | // the Sluggable package, so we control the slug creation/update.
58 | static::observe(PermalinkObserver::class);
59 | }
60 |
61 | /**
62 | * Polymorphic relationship to any entity.
63 | *
64 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo
65 | */
66 | public function entity()
67 | {
68 | return $this->morphTo();
69 | }
70 |
71 | /**
72 | * Relationship to the parent permalink.
73 | *
74 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo+
75 | */
76 | public function parent()
77 | {
78 | return $this->belongsTo(static::class);
79 | }
80 |
81 | /**
82 | * Relationship to the permalink children.
83 | *
84 | * @return \Illuminate\Database\Eloquent\Relations\HasMany
85 | */
86 | public function children()
87 | {
88 | return $this->hasMany(static::class, 'parent_id');
89 | }
90 |
91 | /**
92 | * Return the sluggable configuration array for this model.
93 | *
94 | * @return array
95 | */
96 | public function sluggable(): array
97 | {
98 | if (! $entity = $this->getRelationValue('entity')) {
99 | return [];
100 | }
101 |
102 | $source = (array) $entity->permalinkSlug();
103 |
104 | // We will look for slug source at the permalinkable entity. That method
105 | // should return an array with a 'source' key in it. This way the user
106 | // will be able to provide more parameters to the sluggable options.
107 | return [
108 | 'slug' => array_key_exists('source', $source) ? $source : compact('source')
109 | ];
110 | }
111 |
112 | /**
113 | * Unique slug constraints.
114 | *
115 | * @param Builder $query
116 | * @param Model $model
117 | * @param $attribute
118 | * @param $config
119 | * @param $slug
120 | * @return Builder+
121 | */
122 | public function scopeWithUniqueSlugConstraints(Builder $query, Model $model, $attribute, $config, $slug)
123 | {
124 | if ($slug != '') {
125 | return $query->where('parent_id', $model->parent_id);
126 | }
127 | }
128 |
129 | /**
130 | * Check if the permalink is nested.
131 | *
132 | * @return bool
133 | */
134 | public function isNested()
135 | {
136 | return (bool) ! is_null($this->parent_id);
137 | }
138 |
139 | /**
140 | * Get the default verbs.
141 | *
142 | * @return array
143 | */
144 | public function getMethodAttribute()
145 | {
146 | return ['GET', 'HEAD'];
147 | }
148 |
149 | /**
150 | * Alias to get the entity type.
151 | *
152 | * @return mixed
153 | */
154 | public function getTypeAttribute()
155 | {
156 | return $this->entity_type;
157 | }
158 |
159 | /**
160 | * Convert an alias to a full action path if any.
161 | *
162 | * @return null|string
163 | */
164 | public function getActionAttribute()
165 | {
166 | return app(ActionFactory::class)->resolve($this);
167 | }
168 |
169 | /**
170 | * Replace the action by an alias if any.
171 | *
172 | * @param $value
173 | */
174 | public function setActionAttribute($value)
175 | {
176 | $this->attributes['action'] = array_search($value, static::actionMap()) ?: $value;
177 | }
178 |
179 | /**
180 | * Get the raw action value.
181 | *
182 | * @return mixed
183 | */
184 | public function getRawActionAttribute()
185 | {
186 | return $this->attributes['action'] ?? null;
187 | }
188 |
189 | /**
190 | * Set the parent for from the morph map if exists.
191 | *
192 | * @param $value
193 | */
194 | public function setParentForAttribute($value)
195 | {
196 | if (! Relation::getMorphedModel($value)) {
197 | $value = array_search($value, Relation::morphMap()) ?: $value;
198 | }
199 |
200 | $this->attributes['parent_for'] = $value;
201 | }
202 |
203 | /**
204 | * Set or get the alias map for aliased actions
205 | *
206 | * @param array|null $map
207 | * @param bool $merge
208 | * @return array
209 | */
210 | public static function actionMap(array $map = null, $merge = true)
211 | {
212 | if (! is_null($map)) {
213 | static::$actionMap = $merge && static::$actionMap
214 | ? $map + static::$actionMap : $map;
215 | }
216 |
217 | return static::$actionMap;
218 | }
219 |
220 | /**
221 | * Set all seo values without NULLs.
222 | *
223 | * @param $value
224 | */
225 | public function setSeoAttribute($value)
226 | {
227 | $value = Arr::dot(Arr::wrap($value));
228 |
229 | $this->attributes['seo'] = json_encode(Arr::undot(
230 | array_filter($value, function ($item) {
231 | return $item ?? false;
232 | })
233 | ));
234 | }
235 |
236 | /**
237 | * Get the action associated with a custom alias.
238 | *
239 | * @param string $alias
240 | * @return string|null
241 | */
242 | public static function getMappedAction($alias)
243 | {
244 | return static::$actionMap[$alias] ?? null;
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Advanced Laravel Permalinks and SEO Management from Database
2 |
3 | [](https://travis-ci.com/IsraelOrtuno/permalink) [](https://packagist.org/packages/devio/permalink)
4 |
5 | ## 2021-08-25: Looking for maintainer
6 |
7 | Despite it's pretty stable, I do not have time to keep maintaining this package for future Laravel releases and add more features so I am looking for anyone who found this useful and would like to maintain it. Feel free to contact!
8 | israel@devio.es
9 |
10 | -------
11 |
12 |
13 | This package allows to create dynamic routes right from database, just like WordPress and other CMS do.
14 |
15 | **IMPORTANT** Despite the functionality of this package is not complex at all, there are a few things and good practices to consider. I really recommend to carefully read the entire documentation to deeply understand how this package works as you will be replacing the default Laravel Routing System and do not want to mess up with your URLs and SEO!
16 |
17 | ## Roadmap
18 | - [ ] [Resources for visual SEO management](https://github.com/IsraelOrtuno/permalink-form) (in progress)
19 |
20 | ## Documentation
21 | - [Getting the route for a resource](#getting-the-route-for-a-resource)
22 | - [Automatic SEO generation](#automatic-seo-generation)
23 |
24 | * [Installation](#installation)
25 | * [Getting Started](#getting-started)
26 | * [Replacing the Default Router](#replacing-the-default-router)
27 | * [Creating a Permalink](#creating-permalinks)
28 | * [Updating a Permalink](#updating-permalinks)
29 | * [Binding Models to Permalinks](#binding-models-to-permalinks)
30 | * [Automatically Handling Permalinks](#automatically-handling-permalinks)
31 | * [Nesting Permalinks](#nesting-permalinks)
32 | * [Deleting Permalinks](#deleting-permalinks)
33 | * [Caching Permalinks](#caching.-permalinks)
34 | * [Handling SEO Attributes](#handling-seo-attributes)
35 |
36 | ## Installation
37 |
38 | ### Install the package
39 |
40 | ```shell
41 | composer require devio/permalink
42 | ```
43 |
44 | ### Run the migrations
45 |
46 | ```shell
47 | php artisan migrate
48 | ```
49 |
50 | ## Getting started (PLEASE READ)
51 |
52 | This package handles dynamic routing directly from our database. Nested routes are also supported, so we can easily create routes like this `/jobs/frontend-web-developer`.
53 |
54 | Most of the solutions out there are totally bound to models with polymorphic relationships, however that's not flexible at all when dealing with routes without models. This package supports both, routes with bound models and regular routes.
55 |
56 | Basically, the package stores routes in a `permalinks` table which contains information about every route:
57 | - Slug
58 | - Parent (parent route for nesting)
59 | - Model (if any)
60 | - Action (controller action or model default action)
61 | - SEO options (title, metas...)
62 |
63 | By default, this package will try to find if there's a a permalink in the `permalinks` table matching the current request path in a single SQL query. This is ok for most of the use cases. If for some reason you want to cache your permalinks information into the Laravel Routing Cache, please refer to the [Caching Permalinks](#caching) section.
64 |
65 | ### Example
66 |
67 | Let check out a very basic example to understand how it internally works:
68 |
69 | | id | slug | parent_id | parent_for | entity_type | entity_id | action | final_path |
70 | | -- | ------------- | --------- | ---------- | ------------------ | ---------------- | -------------------- | --------------------- |
71 | | 1 | users | NULL | App\User | NULL | NULL | UserController@index | users
72 | | 2 | israel-ortuno | 1 | NULL | App\User | 1 | UserController@show | users/israel-ortuno
73 |
74 | It will run the following (this example tries to be as explicit as possible, internally it uses eager loading and some other performance optimizations):
75 |
76 | ```php
77 | $router->get('users', 'UserController@index');
78 | $router->get('users/israel-ortuno', 'UserController@show');
79 |
80 | // Which will produce:
81 | // /users UserController@index
82 | // /users/israel-ortuno
83 | ```
84 |
85 | **NOTE:** The `show` method will receive the user as parameter `App\User::find(1)` the route is bound to that model.
86 |
87 | ## Replacing the Default Router
88 | This package has it's own router which extends the default Laravel router. To replace the default router for the one included in this package you have two options:
89 |
90 | ```shell
91 | php artisan permalink:install {--default}
92 | ```
93 |
94 | The console will propmpt you with 2 options:
95 | ```shell
96 | [0] Http/Kernel.php (Default & Recommended)
97 | [1] bootstrap/app.php (Advanced)
98 | ```
99 |
100 | Select the one that fits your needs. For most cases I recommend going through `Http\Kernel.php`. Use the `--default` option to avoid blocking prompts (could also use the default Laravel command's flag `--no-interaction`).
101 |
102 | Both of these methods will replace the default Laravel Router by an extended version provided by this package which contains the Permalink management logic.
103 |
104 | **IMPORTANT:** Use either `Http\Kernel.php` or `bootstrap/app.php`. **Do not** use both as it may cause unexpected behaviour.
105 |
106 | ## Creating Permalinks
107 |
108 | That's pretty much it for setting up the dynamic routing system. Let's create a Permalink record and test it out!
109 |
110 | ```php
111 | Permalink::create([
112 | 'slug' => 'home',
113 | 'action' => 'App\Http\Controllers\HomeController@index'
114 | ]);
115 | // Then visit /home
116 | ```
117 |
118 | If your permalink is bound to a model (read next section), you may create your permalink record as follows:
119 |
120 | ```php
121 | // Note: when using the User::create method, even if permalinkHandling (read more about it below)
122 | // is disabled, it will create the permalink record.
123 | $user = User::create([
124 | 'name' => 'israel',
125 | 'permalink' => [
126 | 'slug' => 'israel-ortuno',
127 | 'action' => 'user.show',
128 | 'seo' => [...] // omit this attribute until you read more about it
129 | ]
130 | ]);
131 |
132 | // Or
133 |
134 | $user->createPermalink([...);
135 | ```
136 |
137 | If you do not provide any data to the `permalink` key when using `User::create` or `createPermalink`, it will automatcally use the default data. Any existing key in the data array will override its default value when creating the permalink.
138 |
139 | **NOTE:** This will only work if `permalinkHandling` has not been disabled, read more about it below.
140 |
141 | ## Updating Peramlinks
142 |
143 | You can easily update a permalink just like any other Eloquent model. **BE CAREFUL** when updating a permalink slug as the previous URL won't be available anymore and this package does not handle 301/302 redirections.
144 |
145 | ### Rebuilding Final Path (PLEASE READ)
146 |
147 | When updating a slug, the package will recursively update its nested permalinks `final_url` attribute reemplacing the previous slug semgment with the new one. You can control this behaviour from the `rebuild_children_on_update` option in your `config/permalink.php` config file. Disable this option if you wish to handle this task manually (NOT RECOMMENDED).
148 |
149 | Check out `Devio\Permalink\Services\PathBuilder` class to discover the methods available for performing the manual update.
150 |
151 | **NOTE:** Make sure to rebuild childen's final path in the current request lifecycle.
152 |
153 | ## Binding Models to Permalinks
154 |
155 | You may want to bind a permalink to a model resource, so you can create a unique URL to access that particular resource. If you want to do so, you just have to use the tait `HasPermalinks` and implement the contract `Permalinkable` to your model.
156 |
157 | ```php
158 | class User extends Model implements \Devio\Permalink\Contracts\Permalinkable;
159 | {
160 | use \Devio\Permalink\HasPermalinks;
161 |
162 | public function permalinkAction()
163 | {
164 | return UserController::class . '@show';
165 | }
166 |
167 | public function permalinkSlug(): array
168 | {
169 | return ['entity.name'];
170 | }
171 | }
172 | ```
173 |
174 | Once you have this setup, this package will generate a permalink for every new record of this model automatically.
175 |
176 | Also, the `Permalinkable` interface will force you to define two simple methods:
177 |
178 | **permalinkAction()**
179 |
180 | This method will return the default controller action responsible for handling the request for this particular model. The model itself will be injected into the action (as Laravel usually does for route model binding).
181 |
182 | ```php
183 | public function show($user)
184 | {
185 | return view('users.show', $user);
186 | }
187 | ```
188 |
189 | **NOTE:** This action will be overwritten by any existing value on the `action` column in your permalink record, so you could have multiple actions for the same model in case you need them.
190 |
191 | **permalinkSlug()**
192 |
193 | This method is a bit more tricky. Since all the slugging task is being handled by the brilliant [Sluggable](https://github.com/cviebrock/eloquent-sluggable) package, we do have to provide the info this package requires on its [sluggable](https://github.com/cviebrock/eloquent-sluggable#updating-your-eloquent-models) method.
194 |
195 | The permalink model will expose an `entity` polymorphic relationship to this model. Since the slugging occurs in the `Permalink` model class, we do have to specify which is going to be the source for our slug. You can consider `entity` as `$this`, so in this case `entity.name` would be equivalent to `$this->name`. Return multiple items if you would like to concatenate multiple properties:
196 |
197 | ```
198 | ['entity.name', 'entity.city']
199 | ```
200 |
201 | **NOTE:** This method should return an array compatible with the Sluggable package, please [check the package documentation](https://github.com/cviebrock/eloquent-sluggable#updating-your-eloquent-models) if you want to go deeper.
202 |
203 | ## Automatically Handling Permalinks
204 |
205 | By default, this package takes care of creating/updating/deleting your permalinks based on the actions performed in the bound model. If you do not want this to happen and want to decide when decide the precise moment the permalink has to be created/updated/deleted for this particular model. You can disable the permalink handling in two ways:
206 |
207 | ```php
208 |
209 | // Temporally disable/enable:
210 | $model->disablePermalinkHandling();
211 | $model->enablePermalinkHandling();
212 |
213 | // Permanent disable or return a condition.
214 | // Create this method in you model:
215 | public function permalinkHanlding()
216 | {
217 | return false;
218 | }
219 | ```
220 |
221 | ### Creating
222 |
223 | A permalink will be created automatically when your resource fires a `saved` event. It will be populate with the default data unless you have provided a `peramlink` key array to the creation array or used the `setPermalinkAttribute` mutator.
224 |
225 | ```php
226 | User::create(['name' => 'israel', 'permalink' => ['slug' => 'israel']]);
227 | //
228 | $user = new User;
229 | $user->permalink = ['slug' => 'israel'];
230 | $user->save();
231 | ```
232 |
233 | If `permalinkHandling` is disabled, you will be able to decide when to create the permalink:
234 |
235 | ```php
236 | // Assume permalinkHanlding() returns false
237 | $user = User::create(['name' => 'israel']);
238 | // Perform other tasks...
239 | $user->createPermalink(); // Array is optional, provide data to override default values
240 | ```
241 |
242 | **NOTE:** Be aware that the permalink record will be still created if the data provided for creation contains a `permalink` key.
243 |
244 | ### Updating
245 |
246 | You can update your permalink right like creating:
247 |
248 | ```php
249 | $user = User::find(1);
250 |
251 | $user->updatePermalink(['seo' => ['title' => 'changed']]);
252 | ```
253 |
254 | **NOTE:** By default, if you update a permalink's slug, it will recursively update all its nested elements with the new segment. Read more about [updating permalinks](#updating-permalinks).
255 |
256 | ### Deleting
257 |
258 | If you delete a resource which is bound to a permalink record, the package will automatically destroy the permalink for us. Again, if you do not want this to happen and want to handle this yourself, disable the permalink handling in your model.
259 |
260 | ### Support for SoftDeleting
261 |
262 | SoftDeleting support comes out of the box, so if your resource is soft deleted, the permalink will be soft deleted too. If you restore your resource, it will be restored automatically too. Disable handling for dealing with this task manually.
263 |
264 | **NOTE:** If you `forceDelete()` your resource, the permalink will also be deleted permanently.
265 |
266 | ## Nesting Permalinks
267 |
268 | You may want to have a nested permalink structure, let's say, for your blog. Parent will be `/blog` and every post should be inside this path, so you can do things like:
269 |
270 | ```
271 | /blog -> Blog index, show all blog posts
272 | /blog/post-1
273 | /blog/post-2
274 | ...
275 | ```
276 |
277 | This package handles this for you out of the box:
278 |
279 | ### Automatic Permalink Nesting
280 |
281 | The `permalinks` table has a column for automatically nesting models: `parent_for`. This attribute should contain the FQN class name of the model you want it to be parent for. Once set, when you create a new permalink for the specified model, it will automatically nested to the given parent.
282 |
283 | This will usually be a manual procedure you will do in you database so it may look like like the [example above](#example).
284 |
285 | ### Disable Automatic Nesting
286 |
287 | If you are deep into this package and want to manage the nesting of your permalinks manually (why would you do so? but just in case...), feel free to disable this feature from the config:
288 |
289 | ```php
290 | // Globally disable this feature for all models in your permalink.php config file
291 | 'nest_to_parent_on_create' => false
292 | // or
293 | config()->set('permalink.nest_to_parent_on_create', false);
294 |
295 | // Disable this feature for a particular model. Define this method in your model class:
296 | public function permalinkNestToParentOnCreate()
297 | {
298 | return false;
299 | }
300 | ```
301 |
302 | ### Manually Nesting
303 |
304 | If you wish to nest a permalink to other manually, all you have to do is to set the `id` of the parent permalink to the `parent_id` attribute on the child permalink:
305 |
306 | ```php
307 | Permalink::create(['slug' => 'my-article', 'parent_id' => 1, 'action' => '...']);
308 | ```
309 |
310 | ## Permalink Actions
311 |
312 | The `action` attribute on your permalink record will be providing the information about what's going to handle the request when that permalink matches the current request URI.
313 |
314 | ### Controllers as Actions
315 |
316 | Every permalink should have a action, specifically those which are not bound to models. You should specify a `controller@action` into the `action` column of your permalink record.
317 |
318 | If there's a model bound to the permalink (entity), it will be passed as parameter to the controller action:
319 |
320 | ```php
321 | class UserController {
322 | public function show($user)
323 | {
324 | return view('users.show', compact('user'));
325 | }
326 | }
327 | ```
328 |
329 | ### Views as Actions
330 |
331 | For simple use cases you could simply specify a view's path as an action for your permalink. The permalink entity (if bound to a model) will also be available in this view as mentioned above:
332 |
333 | ```php
334 | Permalink::create(['slug' => 'users', 'action' => 'users.index']);
335 | ```
336 |
337 | If bound to a model...
338 |
339 | ```php
340 | Permalink::create(['slug' => 'israel-ortuno', 'entity_type' => User::class, 'entity_id' => 1, 'action' => 'users.show']);
341 |
342 | // And then in users/show.blade.php
343 |
Welcome {{ $user->name }}
344 | ```
345 |
346 | #### Using a Custom Controller for View Actions
347 |
348 | Under the hood, view actions are handled by a controller provided by this package `Devio\Permalink\Http\PermalinkController`. You can update this controller with your own implementation if needed. Maybe you want to apply some middleware, or resolve views in a different way...
349 |
350 | All you have to do is to bind your implementation to the container in your `AppServiceProvider` (or other):
351 |
352 | ```php
353 | // In your AppServiceProvider.php
354 | public function register()
355 | {
356 | $this->bind('Devio\Permalink\Http\PermalinkController', YourController::class);
357 | }
358 |
359 | // And then...
360 | class YourController
361 | {
362 | use Devio\Permalink\Http\ResolvesPermalinkView;
363 |
364 | public function __construct()
365 | {
366 | // Do your stuff.
367 | }
368 | }
369 | ```
370 |
371 | This way, Laravel will now resolve your implementation out of the container.
372 |
373 | If you wish to have your own implementation for resolving the views, do not use the `Devio\Permalink\Http\ResolvesPermalinkView` trait and create your own `view()` method.
374 |
375 | ### Default Actions (in Models)
376 |
377 | If you have a model bound to a permalink, you may define a default action in your model like this:
378 |
379 | ```php
380 | public function permalinkAction()
381 | {
382 | return UserController::class . '@show'; // Or a view
383 | }
384 | ```
385 |
386 | This method is mandatory once you implement the `Permalinkable` interface.
387 |
388 | ### Overriding the Default Action
389 |
390 | By default, the permalink will resolve the action based on the `permlainkAction` method of the permalink entity. However, if you specifiy a value to the `action` column in the permalink record, it will override the default action. For example:
391 |
392 | ```php
393 | class User extends Model
394 | {
395 | use HasPermalinks;
396 | ...
397 | public function permalinkAction()
398 | {
399 | return UserController::class . '@index';
400 | }
401 | ...
402 | }
403 |
404 | // And then...
405 | $user = User::create([
406 | 'name' => 'israel',
407 | 'permalink' => [
408 | 'action' => 'user.show'
409 | ]
410 | ]);
411 | // Or just update the action attribute as you like
412 | ```
413 |
414 | When accessing the permalink for this particular entity, `user/show.blade.php` will be responsible for handling the request rather than the default controller. Isn't it cool?
415 |
416 | ## Deleting Permalinks
417 |
418 | By default, and if
419 |
420 | ### Support for SoftDeleting
421 |
422 | ## Caching Permalinks (Read Carefully!)
423 |
424 | As mentioned above, this package will perform a single SQL query on every request in order to find a matching permalink for the current URI. This is quite performant and should be ok for most use cases. This query may also be cached for super-fast access if needed.
425 |
426 | You may cache your permalink routes into the default Laravel Route Caching system, but be aware that it will generate a route for every single record in your `permalinks` table, so I **DO NOT** recommend it if you have a large amount of permalinks, as you may end up with a huge base64 encoded string in your `bootstrap/cache/routes.php` which may really slow down your application bootstrapping. Perform some tests to know if you are really improving performance for the amount of routes you pretend to cache.
427 |
428 | In order to cache you permalinks, all you have to do is to load the entire `permalinks` dataset into the Router and then run the Route Caching command:
429 |
430 | ```php
431 | Router::loadPermalinks();
432 | Artisan::call('route:cache');
433 | ```
434 |
435 | You could create a command to perform this two actions or whatever you consider. From now on, you will have to manually update this cache every time a permalink record has been updated.
436 |
437 | ## Handling SEO Attributes
438 |
439 | This package wouldn't be complete if you could not configure your SEO attributes for every single permalink record, it would have been almost useless!
440 |
441 | ## Automatic SEO generation
442 |
443 | For SEO tags generation [ARCANDEV/SEO-Helper](https://github.com/ARCANEDEV/SEO-Helper) is being used. This package offers a powerful set of tools to manage your SEO meta tags.
444 |
445 | ```
446 | {
447 | "meta": {
448 | "title": "Specific title", // The
449 | "description": "The meta description", // The page meta description
450 | "robots": "noindex,nofollow" // Robots control
451 | },
452 | "opengraph":{
453 | "title": "Specific OG title", // The og:title tag
454 | "description": "The og description", // The og:description tag
455 | "image": "path/to/og-image.jpg" // The og:image tag
456 | },
457 | "twitter":{
458 | "title": "Specific Twitter title", // The twitter:title tag
459 | "description": "The twitter description", // The twitter:description tag
460 | "image": "path/to/og-image.jpg" // The twitter:image tag
461 | }
462 | }
463 | ```
464 |
465 | **NOTE:** This is just an example of the most common tags but you could any kind of tag supported (index, noindex...) by [ARCANDEV/SEO-Helper](https://github.com/ARCANEDEV/SEO-Helper), just make sure to nest it correctly.
466 |
467 | In order to have all this content rendered in your HTML you should add the following you your ``:
468 |
469 | ```blade
470 |
471 | {!! seo_helper()->render() !!}
472 |
473 | ```
474 |
475 | ##### OR
476 |
477 | ```blade
478 |
479 | {{ seo_helper()->renderHtml() }}
480 |
481 | ```
482 |
483 | Plase visit [SEO-Helper – Laravel Usage](https://github.com/ARCANEDEV/SEO-Helper/blob/master/_docs/3-Usage.md#4-laravel-usage) to know more about what and how to render.
484 |
485 | ### Understanding How it Works
486 |
487 | Under the hood, this JSON structure is calling to the different SEO helpers (meta, opengraph and twitter). Let's understand:
488 |
489 | ```json
490 | {
491 | "title": "Generic title",
492 | "image": "path/to/image.jpg",
493 | "description": "Generic description",
494 |
495 | "meta": {
496 | "title": "Default title",
497 | },
498 | "opengraph": {
499 | "image": "path/to/og-image.jpg"
500 | }
501 | }
502 | ```
503 |
504 | This structure will allow you to set a base value for the `title` in all the builders plus changing exclusively the title for the _Meta_ section. Same with the image, Twitter and OpenGraph will inherit the parent image but OpenGraph will replace its for the one on its builder. This way you will be able to display different information on every section!
505 |
506 | This will call [setTitle](https://github.com/ARCANEDEV/SEO-Helper/blob/master/src/Contracts/SeoMeta.php#L127) from the `SeoMeta` helper and [setImage](https://github.com/ARCANEDEV/SEO-Helper/blob/master/src/Contracts/SeoOpenGraph.php#L78) from the `SeoOpenGraph` helper. Same would happen with Twitter. Take some time to review these three contracts in order to know all the methods available:
507 |
508 | - [Metas](https://github.com/ARCANEDEV/SEO-Helper/blob/master/src/Contracts/SeoMeta.php)
509 | - [OpenGraph](https://github.com/ARCANEDEV/SEO-Helper/blob/master/src/Contracts/SeoOpenGraph.php)
510 | - [Twitter](https://github.com/ARCANEDEV/SEO-Helper/blob/master/src/Contracts/SeoTwitter.php)
511 |
512 | In order to match any of the helper methods, every JSON option will be transformed to `studly_case` prefixed by `set` and `add`, so `title` will be converted to `setTitle` and `google_analytics` to `setGoogleAnalytics`. How cool is that?
513 |
514 | All methods are called via `call_user_func_array`, so if an option contains an array, every key will be pased as parameter to the helper method. See `setTitle` or `addWebmaster` which allows multiple parameters.
515 |
516 | ### Populating SEO Attributes
517 |
518 | You can specify the SEO attributes for your permalink by just passing an array of data to the `seo` attribute:
519 |
520 | ```php
521 | Peramlink::create([
522 | 'slug' => 'foo',
523 | 'seo' => [
524 | 'title' => 'this is a title',
525 | 'description' => 'this is a description',
526 | 'opengraph' => [
527 | 'title' => 'this is a custom title for og:title'
528 | ]
529 | ]
530 | );
531 | ```
532 |
533 | #### Populating SEO Attributes with Default Content
534 |
535 | You will usually want to automatically populate your SEO information directly from your bound model information. You can do so by creating fallback methods in you model as shown below:
536 |
537 | ```php
538 | public function getPeramlinkSeoTitleAttribute()
539 | {
540 | return $this->name;
541 | }
542 |
543 | public function getPermalinkSeoOpenGraphTitleAttribute()
544 | {
545 | return $this->name . ' for OpenGraph';
546 | }
547 | ```
548 |
549 | This fallbacks will be used if they indeed exist and the value for that field has not been provided when creating the permalink. Note that these methods should be called as an Eloquent accessor. Use the _permalinkSeo_ prefix and then the path to the default value in a _StudlyCase_, for example:
550 |
551 | ```
552 | seo.title => getPermalinkSeoTitleAttribute()
553 | seo.description => getPermalinkSeoDescriptionAttribute()
554 | seo.twitter.title => getPermalinkSeoTwitterTitleAttribute()
555 | seo.twitter.description => getPermalinkSeoTwitterDescriptionAttribute()
556 | seo.opengraph.title => getPermalinkSeoTwitterOpenGraphAttribute()
557 | seo.opengraph.description => getPermalinkSeoOpenGraphDescriptionAttribute()
558 | ```
559 |
560 | The package will look for any matching method, so you can create as many methods as your seo set-up may need, even if you are just creating custom meta tags so `getPermalinkMyCustomMetaDescriptionAttribute` would match if there's a `seo.my.custom.meta.description` object.
561 |
562 | ### SEO Builders
563 |
564 | To provide even more flexibility, the method calls are piped through 3 classes (one for each helper) called [Builders](https://github.com/IsraelOrtuno/permalink/tree/master/src/Builders). These builders are responsible for calling the right method on the [ARCANDEV/SEO-Helper](https://github.com/ARCANEDEV/SEO-Helper) package.
565 |
566 | If there is a method in this builders matching any of the JSON options, the package will execute that method instead of the default behaviour, which would be calling the method (if exists) from the *SEO-Helper* package.
567 |
568 | Review the [MetaBuilder](https://github.com/IsraelOrtuno/permalink/blob/master/src/Builders/MetaBuilder.php) as example. This builder contains a `setCanonical` method which is basically used as an alias for `setUrl` (just to be more explicit).
569 |
570 | #### Extending Builders
571 |
572 | In order to modify the behaviour of any of these builders, you can create your own Builder which should extend the `Devio\Permalink\Contracts\SeoBuilder` interface or inherit the `Devio\Permalink\Builders\Builder` class.
573 |
574 | Once you have created your own Builder, just replace the default one in the Container. Add the following to the `register` method of any Service Provider in your application:
575 |
576 | ```php
577 | // Singleton or not, whatever you require
578 | $this->app->singleton("permalink.meta", function ($app) { // meta, opengraph, twitter or base
579 | return new MyCustomBuilder;
580 |
581 | // Or if you are inheriting the default builder class
582 |
583 | return (new MyCustomBuilder($app->make(SeoHelper::class)));
584 | });
585 | ```
586 |
587 | If you wish to use other package for generating the SEO meta tags, extending and modifying the builders will do the trick.
588 |
589 | ### Disabling SEO generation
590 |
591 | If you wish to prevent the rendering of any of the three Builders (meta, OpenGraph or Twitter), just set its JSON option to false:
592 |
593 | ```json
594 | {
595 | "meta": { },
596 | "opengraph": false,
597 | "twitter": false
598 | }
599 | ```
600 |
601 | This will disable the execution of the OpenGraph and Twitter builders.
602 |
--------------------------------------------------------------------------------