1
0
Fork 0
mirror of https://github.com/archtechx/laravel-pages.git synced 2025-12-12 01:44:03 +00:00

add source code

This commit is contained in:
Samuel Štancl 2021-08-06 04:31:37 +02:00
commit e534ddd14c
23 changed files with 798 additions and 0 deletions

12
.gitattributes vendored Normal file
View file

@ -0,0 +1,12 @@
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/docker-compose.yml export-ignore
/tests export-ignore
/.php_cs.php export-ignore
/psalm.xml export-ignore
/phpunit.xml export-ignore
/check export-ignore
/coverage export-ignore

39
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,39 @@
name: CI
env:
COMPOSE_INTERACTIVE_NO_CLI: 1
PHP_CS_FIXER_IGNORE_ENV: 1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
on:
push:
pull_request:
branches: [ master ]
jobs:
pest:
name: Tests (Pest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install composer dependencies
run: composer install
- name: Run tests
run: vendor/bin/pest
php-cs-fixer:
name: Code style (php-cs-fixer)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install php-cs-fixer
run: composer global require friendsofphp/php-cs-fixer
- name: Run php-cs-fixer
run: $HOME/.composer/vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php
- name: Commit changes from php-cs-fixer
uses: EndBug/add-and-commit@v5
with:
author_name: "PHP CS Fixer"
author_email: "phpcsfixer@example.com"
message: Fix code style (php-cs-fixer)

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.phpunit.result.cache
package-lock.json
composer.lock
vendor/
.php-cs-fixer.cache
.vscode/
coverage/
node_modules

141
.php-cs-fixer.php Normal file
View file

@ -0,0 +1,141 @@
<?php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$rules = [
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => [
'default' => 'single_space',
'operators' => [
'=>' => null,
'|' => 'no_space',
]
],
'blank_line_after_namespace' => true,
'blank_line_after_opening_tag' => true,
'no_superfluous_phpdoc_tags' => true,
'blank_line_before_statement' => [
'statements' => ['return']
],
'braces' => true,
'cast_spaces' => true,
'class_definition' => true,
'concat_space' => [
'spacing' => 'one'
],
'declare_equal_normalize' => true,
'elseif' => true,
'encoding' => true,
'full_opening_tag' => true,
'declare_strict_types' => true,
'fully_qualified_strict_types' => true, // added by Shift
'function_declaration' => true,
'function_typehint_space' => true,
'heredoc_to_nowdoc' => true,
'include' => true,
'increment_style' => ['style' => 'post'],
'indentation_type' => true,
'linebreak_after_opening_tag' => true,
'line_ending' => true,
'lowercase_cast' => true,
'constant_case' => true,
'lowercase_keywords' => true,
'lowercase_static_reference' => true, // added from Symfony
'magic_method_casing' => true, // added from Symfony
'magic_constant_casing' => true,
'method_argument_space' => true,
'native_function_casing' => true,
'no_alias_functions' => true,
'no_extra_blank_lines' => [
'tokens' => [
'extra',
'throw',
'use',
'use_trait',
]
],
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_closing_tag' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_mixed_echo_print' => [
'use' => 'echo'
],
'no_multiline_whitespace_around_double_arrow' => true,
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line'
],
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_spaces_after_function_name' => true,
'no_spaces_around_offset' => true,
'no_spaces_inside_parenthesis' => true,
'no_trailing_comma_in_list_call' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true,
'no_unneeded_control_parentheses' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_return' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
'normalize_index_brace' => true,
'not_operator_with_successor_space' => true,
'object_operator_without_whitespace' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'phpdoc_indent' => true,
'general_phpdoc_tag_rename' => true,
'phpdoc_no_access' => true,
'phpdoc_no_package' => true,
'phpdoc_no_useless_inheritdoc' => true,
'phpdoc_scalar' => true,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_summary' => true,
'phpdoc_to_comment' => false,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'phpdoc_var_without_name' => true,
'psr_autoloading' => true,
'self_accessor' => true,
'short_scalar_cast' => true,
'simplified_null_return' => false, // disabled by Shift
'single_blank_line_at_eof' => true,
'single_blank_line_before_namespace' => true,
'single_class_element_per_statement' => true,
'single_import_per_statement' => true,
'single_line_after_imports' => true,
'no_unused_imports' => true,
'single_line_comment_style' => [
'comment_types' => ['hash']
],
'single_quote' => true,
'space_after_semicolon' => true,
'standardize_not_equals' => true,
'switch_case_semicolon_to_colon' => true,
'switch_case_space' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline' => true,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
];
$project_path = getcwd();
$finder = Finder::create()
->in([
$project_path . '/src',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new Config())
->setFinder($finder)
->setRules($rules)
->setRiskyAllowed(true)
->setUsingCache(true);

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 ArchTech Development, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

116
README.md Normal file
View file

@ -0,0 +1,116 @@
# Laravel Pages
This package lets you create pages using Markdown or Blade without having to worry about creating routes or controllers yourself.
Essentially, you create either `content/pages/foo.md` or `resources/views/pages/foo.blade.php`.
Markdown files use a pre-defined Blade view to get rendered. Blade files are meant for pages which don't follow the default layout and need more custom styling.
For instance, you could have the `/pricing` route use a Blade file (`pages/pricing.blade.php`) with a pretty design that accompanies your pricing copy.
Whereas for `/about`, you could have a simple Markdown file (`content/pages/about.md`) that describes your service using pure text without any special graphical elements.
We use this on the ArchTech website — the [About](https://archte.ch/about), [Careers](https://archte.ch/careers), and [Open source](https://archte.ch/open-source) pages are simple Markdown files.
## Installation
Require the package via composer:
```
composer require archtechx/laravel-pages
```
Publish the config file:
```
php artisan vendor:publish --tag=archtech-pages-config
```
And finally, add this line to the **end** of your `routes/web.php` file:
```php
ArchTech\Pages\Page::routes();
```
This line will register the routes in a way that ensures that your routes take precedence, and the page route is only used as the final option.
## Usage
### Markdown pages
To create a markdown file, create a file in `content/pages/`. The route to the page will match the file name (without `.md`).
For example, to create the `/about` page, create `content/pages/about.md` with this content:
```md
---
slug: about
title: 'About us'
updated_at: 2021-05-19T19:09:02+00:00
created_at: 2021-05-19T19:09:02+00:00
---
We are a web development agency that specializes in ...
```
### Blade pages
To create a Blade page, create a file in `resources/views/pages/`. Like in the Markdown example, the route to the page will match the file name without the extension.
Therefore to create the `/about` page, you'd create `resources/views/pages/about.blade.php`:
```html
<x-app-layout>
This view can use any layouts or markup.
</x-app-layout>
```
## Configuration
You'll likely want to configure a few things, most likely the used layout.
To do that, simply modify `config/pages.php`.
The config file lets you change:
- the used model
- the used controller
- the layout used by the markdown views
- the view file used to render Markdown pages
- routing details
The layout is used *by* the vendor (package-provided) Markdown view. You'll likely want to set it to something like ``app-layout` or `layouts.app`.`
If you'd like to change the file that renders the Markdown itself, create `resources/views/pages/_markdown.blade.php` (the `_` prefix is important as it prevents direct visits) and change the `pages.views.markdown` config key to `pages._markdown`.
And if you'd like to customize the routing logic more ethan the config file allows you, simply register the route yourself (instead of calling `Page::routes()`):
```php
Route::get('/{page}', ArchTech\Pages\PageController::class);
```
## Ecosystem support
The package perfectly supports other tools in the ecosystem, such as [Laravel Nova](https://nova.laravel.com) or [Lean Admin](https://lean-admin.dev).
For example, in Laravel Nova you could create a resource for the package-provided `Page` model (`ArchTech\Pages\Page`) and use the following field schema:
```php
public function fields(Request $request)
{
return [
Text::make('slug'),
Text::make('title'),
Markdown::make('content'),
];
}
```
## Git integration & Orbit
This package uses [Orbit](https://github.com/ryangjchandler/orbit) under the hood — to manage the Markdown files as Eloquent models. If you'd like to customize some things related to that logic, take a look at the Orbit documentation.
The package also uses another package of ours, [Laravel SEO](https://github.com/archtechx/laravel-seo), to provide meta tag support for Markdown pages. We recommended that you use this package yourself, since it will make handling meta tags as easy as adding the following line to your layout's `<head>` section:
```html
<x-seo::meta />
```

36
assets/config.php Normal file
View file

@ -0,0 +1,36 @@
<?php
return [
'model' => ArchTech\Pages\Page::class,
'views' => [
/**
* The layout used to render the pages.
*
* @example app-layout For resources/views/app-layout.blade.php
* @example layouts.app For resources/views/layouts.app.blade.php
*/
'layout' => 'app-layout',
/**
* The path to your views.
*
* @example 'pages.' The package will look into resources/views/pages
* @example 'foo::' The package will look into the 'foo' view namespace
*/
'path' => 'pages.',
/**
* The name of the view used to render markdown pages.
*
* @example 'pages._markdown' The package will use resources/views/pages/_markdown.blade.php
*/
'markdown' => 'pages::_markdown',
],
'routes' => [
'name' => 'page',
'prefix' => '',
'handler' => ArchTech\Pages\PageController::class,
],
];

View file

@ -0,0 +1,9 @@
<x-dynamic-component :component="config('pages.views.layout')">
<div class="w-full flex justify-center my-16 px-4">
<div class="prose prose-indigo">
<h1>{{ $page->title }}</h1>
{!! Str::markdown($page->content) !!}
</div>
</div>
</x-dynamic-component>

47
check Executable file
View file

@ -0,0 +1,47 @@
#!/bin/bash
set -e
offer_run() {
read -p "For more output, run $1. Run it now (Y/n)? " run
case ${run:0:1} in
n|N )
exit 1
;;
* )
$1
;;
esac
exit 1
}
if (php-cs-fixer fix --dry-run --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then
echo '✅ php-cs-fixer OK'
else
read -p "⚠️ php-cs-fixer found issues. Fix (Y/n)? " fix
case ${fix:0:1} in
n|N )
echo '❌ php-cs-fixer FAIL'
offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php'
;;
* )
if (php-cs-fixer fix --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then
echo '✅ php-cs-fixer OK'
else
echo '❌ php-cs-fixer FAIL'
offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php'
fi
;;
esac
fi
if (./vendor/bin/pest > /dev/null 2>/dev/null); then
echo '✅ PEST OK'
else
echo '❌ PEST FAIL'
offer_run './vendor/bin/pest'
fi
echo '=================='
echo '✅ Everything OK'

42
composer.json Normal file
View file

@ -0,0 +1,42 @@
{
"name": "archtechx/laravel-pages",
"description": "",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Samuel Štancl",
"email": "samuel@archte.ch"
}
],
"autoload": {
"psr-4": {
"ArchTech\\Pages\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ArchTech\\Pages\\Tests\\": "tests/"
}
},
"require": {
"php": "^8.0",
"illuminate/support": "^8.24",
"archtechx/laravel-seo": "^0.2.2",
"ryangjchandler/orbit": "^0.9.0",
"illuminate/routing": "^8.53",
"illuminate/database": "^8.53"
},
"require-dev": {
"orchestra/testbench": "^6.9",
"pestphp/pest": "^1.2",
"pestphp/pest-plugin-laravel": "^1.0"
},
"extra": {
"laravel": {
"providers": [
"ArchTech\\Pages\\PackageServiceProvider"
]
}
}
}

33
phpunit.xml Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
<report>
<clover outputFile="coverage/phpunit/clover.xml"/>
<html outputDirectory="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/>
</report>
</coverage>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_KEY" value="base64:uYlmYxcuuO7dC34yUn2hQcPu8PnlC98LTyOZg4fNAZU="/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="redis"/>
<env name="MAIL_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="testbench"/>
<env name="DB_DATABASE" value="main"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
</php>
</phpunit>

41
src/Page.php Normal file
View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace ArchTech\Pages;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Route;
use Orbit\Concerns\Orbital;
class Page extends Model
{
use Orbital;
protected $guarded = [];
public static function schema(Blueprint $table)
{
$table->string('slug');
$table->string('title');
$table->longText('content');
}
public function getKeyName()
{
return 'slug';
}
public function getIncrementing()
{
return false;
}
public static function routes(): void
{
Route::get('/{page}', config('pages.routes.handler'))
->prefix(config('pages.routes.prefix'))
->name(config('pages.routes.name'));
}
}

33
src/PageController.php Normal file
View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace ArchTech\Pages;
use Illuminate\Support\Str;
class PageController
{
public function __invoke(string $page)
{
if (str_starts_with($page, '_')) {
abort(404);
}
$view = config('pages.views.path') . $page;
if (view()->exists($view)) {
return view($view);
}
if ($model = config('pages.model')::find($page)) {
seo()
->title($model->title)
->description(Str::limit($model->content, 100));
return view(config('pages.views.markdown'), ['page' => $model]);
}
abort(404);
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace ArchTech\Pages;
use Illuminate\Support\ServiceProvider;
class PagesServiceProvider extends ServiceProvider
{
public function register(): void
{
}
public function boot(): void
{
$this->loadViewsFrom(__DIR__ . '/../assets/views', 'pages');
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'pages');
$this->publishes([
__DIR__ . '/../assets/views' => resource_path('views/vendor/pages'),
], 'archtech-pages-views');
$this->publishes([
__DIR__ . '/../assets/config.php' => config_path('pages.php'),
], 'archtech-pages-config');
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
trait CreatesApplication
{
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
}

View file

@ -0,0 +1,79 @@
<?php
use ArchTech\Pages\Page;
beforeEach(function () {
Page::routes();
Page::query()->delete();
view()->addNamespace('test', __DIR__ . '/../views');
config([
'pages.views.layout' => 'test::layout',
'pages.views.path' => 'test::',
'orbit.paths.content' => __DIR__ . '/../orbit/content',
'orbit.paths.cache' => __DIR__ . '/../orbit/cache',
]);
});
test('a view is shown if it exists')
->get('/example')
->assertSee('Test view');
test('markdown is rendered if it exists', function () {
Page::create([
'slug' => 'test',
'title' => 'Markdown page',
'content' => 'This is a **test page**'
]);
using($this)
->get('/test')
->assertSee('Markdown page')
->assertSee('<strong>test page</strong>', false);
});
test('view takes precedence over markdown', function () {
Page::create([
'slug' => 'example',
'title' => 'Test page',
'content' => 'This is a test page'
]);
using($this)
->get('/example')
->assertSee('Test view')
->assertDontSee('Test page');
});
test('404 is returned if no view or markdown is found')
->get('/foo')
->assertNotFound();
test('a custom layout can be used', function () {
config(['pages.views.layout' => 'test::layout2']);
Page::create([
'slug' => 'test',
'title' => 'Test page',
'content' => 'This is a test page'
]);
using($this)
->get('/test')
->assertSee('second layout');
});
test('SEO metadata is set on markdown pages', function () {
Page::create([
'slug' => 'test',
'title' => 'Test page',
'content' => 'This is a test page'
]);
using($this)
->get('/test')
->assertSee('<meta property="og:title" content="Test page" />', false)
->assertSee('<meta property="og:description" content="This is a test page" />', false);
});

47
tests/Pest.php Normal file
View file

@ -0,0 +1,47 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
use ArchTech\Pages\Tests\TestCase;
uses(ArchTech\Pages\Tests\TestCase::class)->in('Feature');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/
function using($test): TestCase
{
return $test;
}

20
tests/TestCase.php Normal file
View file

@ -0,0 +1,20 @@
<?php
namespace ArchTech\Pages\Tests;
use Orchestra\Testbench\TestCase as TestbenchTestCase;
use ArchTech\Pages\PagesServiceProvider;
use ArchTech\SEO\SEOServiceProvider;
use Orbit\OrbitServiceProvider;
class TestCase extends TestbenchTestCase
{
protected function getPackageProviders($app)
{
return [
SEOServiceProvider::class,
OrbitServiceProvider::class,
PagesServiceProvider::class,
];
}
}

View file

@ -0,0 +1,7 @@
---
slug: example
title: 'Test page'
updated_at: 2021-08-06T02:25:15+00:00
created_at: 2021-08-06T02:25:15+00:00
---
This is a test page

View file

@ -0,0 +1,7 @@
---
slug: test
title: 'Test page'
updated_at: 2021-08-06T02:25:15+00:00
created_at: 2021-08-06T02:25:15+00:00
---
This is a test page

View file

@ -0,0 +1,4 @@
<div class="first layout">
<x-seo::meta />
{{ $slot }}
</div>

View file

@ -0,0 +1,3 @@
<div class="second layout">
{{ $slot }}
</div>

View file

@ -0,0 +1,3 @@
<x-test::layout>
Test view
</x-test::layout>