├── log
└── .gitkeep
├── temp
└── .gitkeep
├── .gitignore
├── config
├── users.neon
├── front.neon
├── admin.neon
├── model.neon
└── main.neon
├── public
├── .htaccess
└── index.php
├── storage
└── articles
│ ├── random-article.md
│ └── hello-world.md
├── app
├── Module
│ ├── Front
│ │ ├── @Templates
│ │ │ ├── Error4xx
│ │ │ │ ├── 4xx.latte
│ │ │ │ ├── 405.latte
│ │ │ │ ├── 410.latte
│ │ │ │ ├── 403.latte
│ │ │ │ └── 404.latte
│ │ │ ├── Auth
│ │ │ │ └── default.latte
│ │ │ ├── Article
│ │ │ │ └── default.latte
│ │ │ ├── Homepage
│ │ │ │ └── default.latte
│ │ │ ├── Error
│ │ │ │ ├── 503.phtml
│ │ │ │ └── 500.phtml
│ │ │ └── @Layout
│ │ │ │ ├── header.latte
│ │ │ │ └── layout.latte
│ │ ├── Auth
│ │ │ ├── AuthTemplate.php
│ │ │ ├── Form
│ │ │ │ └── SignInFormFactory.php
│ │ │ └── AuthPresenter.php
│ │ ├── Error4xx
│ │ │ ├── Error4xxTemplate.php
│ │ │ └── Error4xxPresenter.php
│ │ ├── Article
│ │ │ ├── ArticleTemplate.php
│ │ │ └── ArticlePresenter.php
│ │ ├── BaseFrontTemplate.php
│ │ ├── Homepage
│ │ │ ├── HomepageTemplate.php
│ │ │ └── HomepagePresenter.php
│ │ ├── BaseFrontPresenter.php
│ │ └── Error
│ │ │ └── ErrorPresenter.php
│ └── Admin
│ │ ├── Dashboard
│ │ ├── DashboardTemplate.php
│ │ └── DashboardPresenter.php
│ │ ├── @Templates
│ │ ├── Article
│ │ │ ├── edit.latte
│ │ │ └── default.latte
│ │ ├── Dashboard
│ │ │ └── default.latte
│ │ └── @Layout
│ │ │ ├── header.latte
│ │ │ └── layout.latte
│ │ ├── BaseAdminTemplate.php
│ │ ├── Article
│ │ ├── ArticleTemplate.php
│ │ ├── Form
│ │ │ └── ArticleFormFactory.php
│ │ └── ArticlePresenter.php
│ │ └── BaseAdminPresenter.php
├── Model
│ └── Article
│ │ ├── Exception
│ │ └── ArticleNotFoundException.php
│ │ ├── ArticleData.php
│ │ ├── ArticleFacade.php
│ │ ├── ArticleDataFactory.php
│ │ ├── Article.php
│ │ └── ArticleRepository.php
├── Bootstrap.php
├── Router
│ └── RouterFactory.php
└── UI
│ └── Form
│ └── Renderer
│ └── BootstrapRenderer.php
├── README.md
└── composer.json
/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/temp/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | /composer.lock
4 |
--------------------------------------------------------------------------------
/config/users.neon:
--------------------------------------------------------------------------------
1 | security:
2 | users:
3 | nette: framework
4 |
--------------------------------------------------------------------------------
/config/front.neon:
--------------------------------------------------------------------------------
1 | search:
2 | front:
3 | in: %appDir%/Module/Front
4 | classes:
5 | - *Factory
6 | - *Component
7 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 | RewriteEngine On
2 | RewriteCond %{REQUEST_FILENAME} !-f
3 | RewriteCond %{REQUEST_FILENAME} !-d
4 | RewriteRule ^(.*)$ index.php [L]
5 |
--------------------------------------------------------------------------------
/config/admin.neon:
--------------------------------------------------------------------------------
1 | search:
2 | admin:
3 | in: %appDir%/Module/Admin
4 | classes:
5 | - *Factory
6 | - *Component
7 |
--------------------------------------------------------------------------------
/storage/articles/random-article.md:
--------------------------------------------------------------------------------
1 | ```yaml
2 | title: Random Article
3 | author: Rixafy
4 | createdAt: '2020-10-10 10:00'
5 | ```
6 |
7 | Hi, **this** is very random `article`
8 |
--------------------------------------------------------------------------------
/config/model.neon:
--------------------------------------------------------------------------------
1 | search:
2 | model:
3 | in: %appDir%/Model
4 | classes:
5 | - *Facade
6 | - *Factory
7 | - *Repository
8 | - *Provider
9 | - *Manager
10 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Error4xx/4xx.latte:
--------------------------------------------------------------------------------
1 | {block content}
2 |
Oops...
3 |
4 | Your browser sent a request that this server could not understand or process.
5 |
--------------------------------------------------------------------------------
/storage/articles/hello-world.md:
--------------------------------------------------------------------------------
1 | ```yaml
2 | title: Hello world
3 | author: Rixafy
4 | createdAt: '2020-10-10 10:00'
5 | ```
6 |
7 | This is `Hello World` article *written* using **markdown** syntax
8 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Error4xx/405.latte:
--------------------------------------------------------------------------------
1 | {block content}
2 | Method Not Allowed
3 |
4 | The requested method is not allowed for the URL.
5 |
6 | error 405
7 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Error4xx/410.latte:
--------------------------------------------------------------------------------
1 | {block content}
2 | Page Not Found
3 |
4 | The page you requested has been taken off the site. We apologize for the inconvenience.
5 |
6 | error 410
7 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | createContainer()
9 | ->getByType(Nette\Application\Application::class)
10 | ->run();
11 |
--------------------------------------------------------------------------------
/app/Module/Front/Auth/AuthTemplate.php:
--------------------------------------------------------------------------------
1 |
4 | Edit article {$article->getTitle()}
5 |
6 | {control articleForm}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Error4xx/403.latte:
--------------------------------------------------------------------------------
1 | {block content}
2 | Access Denied
3 |
4 | You do not have permission to view this page. Please try contact the web
5 | site administrator if you believe you should be able to view this page.
6 |
7 | error 403
8 |
--------------------------------------------------------------------------------
/app/Module/Front/Article/ArticleTemplate.php:
--------------------------------------------------------------------------------
1 |
4 | Dashboard
5 | There is nothing to see, try to create an article or return to the homepage
6 |
7 |
--------------------------------------------------------------------------------
/app/Model/Article/ArticleData.php:
--------------------------------------------------------------------------------
1 |
3 | Page Not Found
4 |
5 | The page you requested could not be found. It is possible that the address is
6 | incorrect, or that the page no longer exists. Please use a search engine to find
7 | what you are looking for.
8 |
9 | error 404
10 |
11 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Auth/default.latte:
--------------------------------------------------------------------------------
1 | {templateType App\Module\Front\Auth\AuthTemplate}
2 | {block content}
3 |
4 |
Account
5 |
6 | Username: nette
7 | Password: framework
8 |
9 | {if $user->isLoggedIn()}
10 |
Log out
11 | {else}
12 |
13 |
14 | {control signInForm}
15 |
16 |
17 | {/if}
18 |
19 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Article/default.latte:
--------------------------------------------------------------------------------
1 | {templateType App\Module\Front\Article\ArticleTemplate}
2 | {block content}
3 |
4 |
{$article->getTitle()}
5 |
{$article->getAuthor()}, {$article->getCreatedAt()->format('Y-m-d H:i:s')} [edit]
6 |
{$article->getContent()|noescape}
7 |
last changed at {$article->getUpdatedAt()->format('Y-m-d H:i:s')}
8 |
9 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Homepage/default.latte:
--------------------------------------------------------------------------------
1 | {templateType App\Module\Front\Homepage\HomepageTemplate}
2 | {block content}
3 |
4 |
5 |
6 |
{$article->getAuthor()}, {$article->getCreatedAt()->format('Y-m-d H:i:s')} [edit]
7 |
{$article->getContent()|noescape}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/Module/Front/Auth/Form/SignInFormFactory.php:
--------------------------------------------------------------------------------
1 | addText('username', 'Username')
17 | ->setRequired();
18 |
19 | $form->addPassword('password', 'Password')
20 | ->setRequired();
21 |
22 | $form->addSubmit('submit', 'Log in');
23 |
24 | $form->setRenderer(new BootstrapRenderer());
25 |
26 | return $form;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Bootstrap.php:
--------------------------------------------------------------------------------
1 | setDebugMode(true)
17 | ->setTimeZone('Europe/Prague')
18 | ->setTempDirectory(__DIR__ . '/../temp');
19 |
20 | $configurator->addConfig(__DIR__ . '/../config/main.neon');
21 |
22 | $configurator->enableTracy(__DIR__ . '/../log');
23 |
24 | return $configurator;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Module/Admin/@Templates/Article/default.latte:
--------------------------------------------------------------------------------
1 | {templateType App\Module\Admin\Article\ArticleTemplate}
2 | {block title}Articles{/block}
3 | {block content}
4 |
5 |
6 |
7 |
Create new article
8 | {control articleForm}
9 |
10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Error/503.phtml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 | Site is temporarily down for maintenance
21 |
22 | We're Sorry
23 |
24 | The site is temporarily down for maintenance. Please try again in a few minutes.
25 |
--------------------------------------------------------------------------------
/app/Module/Front/Error4xx/Error4xxPresenter.php:
--------------------------------------------------------------------------------
1 | getName())[1] . '/' . $this->getHttpResponse()->getCode() .'.latte';
19 | if (!file_exists($file)) {
20 | $this->getTemplate()->setFile(__DIR__ . '/../@Templates/' . Helpers::splitName($this->getName())[1] . '/4xx.latte');
21 | } else {
22 | $this->getTemplate()->setFile($file);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Module/Front/Homepage/HomepagePresenter.php:
--------------------------------------------------------------------------------
1 | articles = $this->articleFacade->getAll();
28 | }
29 |
30 | public function renderDefault(): void
31 | {
32 | $this->template->articles = $this->articles;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nette flat-file blog example
2 | This repository demonstrates how to use multiple modules in nette application
3 |
4 | ## Run the blog
5 | Minimum required PHP version is **8.0**
6 | ```bash
7 | composer update
8 | ```
9 | ```bash
10 | php -S localhost:80 -t public/
11 | ```
12 |
13 | ## What can you learn?
14 | - Automatically registering services using SearchExtension
15 | - How to design an optimal skeleton for bigger applications
16 | - How to create links between multiple modules
17 | - Using a better module mapping (in /config/main.neon)
18 | - How to use nette/forms with factories, and where to put handling logic
19 | - Creating custom form renderer
20 | - Neon config files can be separated and included to main file
21 | - How to change layout and template paths (in base presenters)
22 | - Router can be used as a service
23 | - How to delegate model logic to keep codebase clean
24 |
--------------------------------------------------------------------------------
/app/Module/Admin/Article/Form/ArticleFormFactory.php:
--------------------------------------------------------------------------------
1 | addText('title', 'Title')
18 | ->setRequired();
19 |
20 | $form->addTextArea('content', 'Content')
21 | ->setRequired();
22 |
23 | $form->addText('author', 'Author')
24 | ->setRequired();
25 |
26 | if ($article !== null) {
27 | $form->addSubmit('submit', 'Save');
28 | $form->setDefaults($article->getData());
29 |
30 | } else {
31 | $form->addSubmit('submit', 'Create');
32 | }
33 |
34 | $form->setRenderer(new BootstrapRenderer());
35 |
36 | return $form;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Module/Front/BaseFrontPresenter.php:
--------------------------------------------------------------------------------
1 | setLayout(__DIR__ . '/@Templates/@Layout/layout.latte');
19 | $this->getTemplate()->setFile(__DIR__ . '/@Templates/' . Helpers::splitName($this->getName())[1] . '/' . $this->getAction() .'.latte');
20 |
21 | // Here is possible to assign common variables among presenters, user is just an example, nette would automatically passed user to template anyway
22 | $this->template->user = $this->getUser();
23 | }
24 |
25 | /**
26 | * @throws AbortException
27 | */
28 | public function handleLogout(): void
29 | {
30 | $this->getUser()->logout();
31 | $this->redirect('this');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nette/simple-blog",
3 | "description": "Simple flat-file blog using nette framework",
4 | "type": "project",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Rixafy",
9 | "email": "rixafy@gmail.com"
10 | }
11 | ],
12 | "minimum-stability": "stable",
13 | "require": {
14 | "php": ">=8.0",
15 | "nette/application": "^3.1",
16 | "nette/bootstrap": "^3.1",
17 | "nette/caching": "^3.0",
18 | "nette/di": "^3.0",
19 | "nette/finder": "^2.5",
20 | "nette/forms": "^3.1",
21 | "nette/http": "^3.1",
22 | "nette/security": "^3.1",
23 | "nette/utils": "^3.2",
24 | "tracy/tracy": "^2.9",
25 | "latte/latte": "^3.0",
26 | "erusev/parsedown": "^1.7"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "App\\": "app"
31 | }
32 | },
33 | "config": {
34 | "platform": {
35 | "php": "8.0"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/@Layout/header.latte:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/app/Module/Admin/@Templates/@Layout/header.latte:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/app/Module/Front/Article/ArticlePresenter.php:
--------------------------------------------------------------------------------
1 | article = $this->articleFacade->get($slug);
33 | } catch (ArticleNotFoundException) {
34 | $this->error('Article not found', 404);
35 | }
36 | }
37 |
38 | public function renderDefault(string $slug): void
39 | {
40 | $this->template->article = $this->article;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Router/RouterFactory.php:
--------------------------------------------------------------------------------
1 | add($this->createAdminRouter());
17 | $router->add($this->createFrontRouter());
18 |
19 | return $router;
20 | }
21 |
22 | private function createFrontRouter(): RouteList
23 | {
24 | $frontRouter = new RouteList('Front');
25 |
26 | $frontRouter->addRoute('/blog/', [
27 | Presenter::PRESENTER_KEY => 'Article',
28 | Presenter::ACTION_KEY => 'default',
29 | ]);
30 |
31 | $frontRouter->addRoute('/[][/][/]');
32 |
33 | return $frontRouter;
34 | }
35 |
36 | private function createAdminRouter(): RouteList
37 | {
38 | $adminRouter = new RouteList('Admin');
39 |
40 | $adminRouter->addRoute('admin[/][/][/]');
41 |
42 | return $adminRouter;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Module/Front/Auth/AuthPresenter.php:
--------------------------------------------------------------------------------
1 | signInFormFactory->create();
26 |
27 | $form->onSuccess[] = function (array $data) {
28 | try {
29 | $this->getUser()->login($data['username'], $data['password']);
30 | $this->flashMessage('Successfully logged in', 'success');
31 |
32 | } catch (AuthenticationException) {
33 | $this->flashMessage('Bad credentials', 'danger');
34 | }
35 |
36 | $this->redirect('this');
37 | };
38 |
39 | return $form;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Module/Admin/@Templates/@Layout/layout.latte:
--------------------------------------------------------------------------------
1 | {templateType App\Module\Front\BaseFrontTemplate}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {ifset title}{include title|stripHtml|trim} | {/ifset}Simple Blog
9 |
10 |
11 |
12 |
13 |
14 | {include 'header.latte'}
15 |
16 |
17 |
{$flash->message}
18 |
19 |
20 | {include content}
21 |
22 | {block scripts}
23 |
24 | {/block}
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/@Layout/layout.latte:
--------------------------------------------------------------------------------
1 | {templateType App\Module\Front\BaseFrontTemplate}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {ifset title}{include title|stripHtml|trim} | {/ifset}Simple Blog
9 |
10 |
11 |
12 |
13 |
14 | {include 'header.latte'}
15 |
16 |
17 |
{$flash->message}
18 |
19 |
20 | {include content}
21 |
22 | {block scripts}
23 |
24 | {/block}
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/Model/Article/ArticleFacade.php:
--------------------------------------------------------------------------------
1 | save($article);
18 |
19 | return $article;
20 | }
21 |
22 | /**
23 | * @throws ArticleNotFoundException
24 | */
25 | public function edit(string $slug, ArticleData $data): void
26 | {
27 | $article = $this->get($slug);
28 |
29 | $article->edit($data);
30 |
31 | $this->save($article);
32 | }
33 |
34 | private function save(Article $article): void
35 | {
36 | FileSystem::write(__DIR__ . '/../../../storage/articles/' . $article->getSlug() . '.md', "```yaml\n" . Neon::encode([
37 | 'title' => $article->getTitle(),
38 | 'author' => $article->getAuthor(),
39 | 'createdAt' => $article->getCreatedAt()->format('Y-m-d H:i:s')
40 | ], Neon::BLOCK) . "```\n\n" . $article->getData()->content . "\n");
41 | }
42 |
43 | public function delete(string $slug): void
44 | {
45 | @unlink(__DIR__ . '/../../../storage/articles/' . $slug . '.md');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/Module/Admin/BaseAdminPresenter.php:
--------------------------------------------------------------------------------
1 | getUser()->isLoggedIn()) {
25 | $this->flashMessage('You shall not pass', 'danger');
26 | $this->redirect(':Front:Auth:default');
27 | }
28 | }
29 |
30 | public function beforeRender(): void
31 | {
32 | $this->setLayout(__DIR__ . '/@Templates/@Layout/layout.latte');
33 | $this->getTemplate()->setFile(__DIR__ . '/@Templates/' . Helpers::splitName($this->getName())[1] . '/' . $this->getAction() .'.latte');
34 |
35 | // Here is possible to assign common variables among presenters, user is just an example, nette would automatically passed user to template anyway
36 | $this->template->user = $this->getUser();
37 | }
38 |
39 | /**
40 | * @throws AbortException
41 | */
42 | public function handleLogout(): void
43 | {
44 | $this->getUser()->logout();
45 | $this->redirect('this');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/Module/Front/@Templates/Error/500.phtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Server Error
5 |
6 |
13 |
14 |
15 |
16 |
Server Error
17 |
18 |
We're sorry! The server encountered an internal error and
19 | was unable to complete your request. Please try again later.
20 |
21 |
error 500
22 |
23 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/app/Model/Article/ArticleDataFactory.php:
--------------------------------------------------------------------------------
1 | getPathname());
18 |
19 | preg_match('/```yaml(.*?)```/s', $fileContent, $match);
20 | $metaData = Neon::decode($match[1]);
21 |
22 | $data = new ArticleData();
23 | $data->slug = $file->getBasename('.md');
24 | $data->title = $metaData['title'];
25 | $data->author = $metaData['author'];
26 | $data->content = trim(str_replace($match[0], '', $fileContent));
27 | $data->createdAt = DateTime::from($metaData['createdAt']);
28 | $data->updatedAt = DateTime::from($file->getMTime());
29 |
30 | return $data;
31 | }
32 |
33 | public function createFromFormData(array $formData): ArticleData
34 | {
35 | $data = new ArticleData();
36 |
37 | $data->slug = Strings::webalize($formData['title']);
38 | $data->title = $formData['title'];
39 | $data->author = $formData['author'];
40 | $data->content = $formData['content'];
41 | $data->createdAt = new DateTime();
42 | $data->updatedAt = new DateTime();
43 |
44 | return $data;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/Module/Front/Error/ErrorPresenter.php:
--------------------------------------------------------------------------------
1 | getParameter('exception');
29 |
30 | if ($exception instanceof BadRequestException) {
31 | [$module, , $sep] = Helpers::splitName($request->getPresenterName());
32 | return new ForwardResponse($request->setPresenterName($module . $sep . 'Error4xx'));
33 | }
34 |
35 | $this->logger->log($exception, ILogger::EXCEPTION);
36 |
37 | return new CallbackResponse(function (IRequest $httpRequest, IResponse $httpResponse): void {
38 | if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) {
39 | $file = __DIR__ . '/../@Templates/Error/500.phtml';
40 | if (is_file($file)) {
41 | echo file_get_contents($file);
42 | } else {
43 | echo 'Error 500';
44 | }
45 | }
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/Model/Article/Article.php:
--------------------------------------------------------------------------------
1 | slug = $data->slug;
22 | $this->createdAt = $data->createdAt;
23 | $this->edit($data);
24 | }
25 |
26 | public function edit(ArticleData $data)
27 | {
28 | $this->title = $data->title;
29 | $this->author = $data->author;
30 | $this->content = $data->content;
31 | $this->updatedAt = $data->updatedAt;
32 | }
33 |
34 | public function getData(): ArticleData
35 | {
36 | $data = new ArticleData();
37 | $data->title = $this->title;
38 | $data->author = $this->author;
39 | $data->content = $this->content;
40 |
41 | return $data;
42 | }
43 |
44 | public function getSlug(): string
45 | {
46 | return $this->slug;
47 | }
48 |
49 | public function getTitle(): string
50 | {
51 | return $this->title;
52 | }
53 |
54 | public function getAuthor(): string
55 | {
56 | return $this->author;
57 | }
58 |
59 | public function getContent(): string
60 | {
61 | return (new Parsedown())->parse($this->content);
62 | }
63 |
64 | public function getCreatedAt(): DateTime
65 | {
66 | return $this->createdAt;
67 | }
68 |
69 | public function getUpdatedAt(): DateTime
70 | {
71 | return $this->updatedAt;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/Model/Article/ArticleRepository.php:
--------------------------------------------------------------------------------
1 | articleDataFactory->createFromFile(new SplFileInfo($path)));
29 | }
30 |
31 | /**
32 | * @return Article[]
33 | */
34 | public function getAll(): array
35 | {
36 | $articles = [];
37 |
38 | /** @var SplFileInfo $file */
39 | foreach (Finder::findFiles('*.md')->from(__DIR__ . '/../../../storage/articles') as $file) {
40 | $articles[] = new Article($this->articleDataFactory->createFromFile($file));
41 | }
42 |
43 | return $articles;
44 | }
45 |
46 | /**
47 | * @return Article[]
48 | */
49 | public function getSearchResults(string $query): array
50 | {
51 | $articles = [];
52 |
53 | foreach ($this->getAll() as $article) {
54 | $match = similar_text($lowerQuery = strtolower($query), $lowerTitle = strtolower($article->getTitle()));
55 | if (str_contains($lowerTitle, $lowerQuery) || $match >= 3.5) {
56 | $articles[] = $article;
57 | }
58 | }
59 |
60 | return $articles;
61 | }
62 |
63 | public function getAllSlugs(): array
64 | {
65 | $slugs = [];
66 |
67 | /** @var SplFileInfo $file */
68 | foreach (Finder::findFiles('*.md')->from(__DIR__ . '/../../../storage/articles') as $file) {
69 | $slugs[] = $file->getBasename('.md');
70 | }
71 |
72 | return $slugs;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/Module/Admin/Article/ArticlePresenter.php:
--------------------------------------------------------------------------------
1 | articles = $this->articleFacade->getAll();
35 | }
36 |
37 | public function renderDefault(): void
38 | {
39 | $this->template->articles = $this->articles;
40 | }
41 |
42 | /**
43 | * @throws ArticleNotFoundException
44 | */
45 | public function actionEdit(string $slug): void
46 | {
47 | $this->article = $this->articleFacade->get($slug);
48 | }
49 |
50 | public function renderEdit(string $slug): void
51 | {
52 | $this->template->article = $this->article;
53 | }
54 |
55 | public function createComponentArticleForm(): Form
56 | {
57 | $form = $this->articleFormFactory->create($this->article);
58 |
59 | $form->onSuccess[] = function (array $data): void {
60 | if ($this->article !== null) {
61 | $this->articleFacade->edit($this->article->getSlug(), $this->articleDataFactory->createFromFormData($data));
62 | $this->flashMessage('Article was successfully updated', 'success');
63 |
64 | } else {
65 | $this->articleFacade->create($this->articleDataFactory->createFromFormData($data));
66 | $this->flashMessage('Article was successfully created', 'success');
67 | }
68 |
69 | $this->redirect('this');
70 | };
71 |
72 | return $form;
73 | }
74 |
75 | /**
76 | * @throws AbortException
77 | */
78 | public function handleDelete(string $slug): void
79 | {
80 | $this->articleFacade->delete($slug);
81 | $this->flashMessage('Article was successfully deleted', 'success');
82 | $this->redirect('this');
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/UI/Form/Renderer/BootstrapRenderer.php:
--------------------------------------------------------------------------------
1 | wrappers['controls']['container'] = null;
25 | $this->wrappers['pair']['container'] = 'div class="form-group row mb-4"';
26 | $this->wrappers['pair']['.error'] = 'has-error';
27 | $this->wrappers['control']['container'] = 'div class=col-sm-12';
28 | $this->wrappers['label']['container'] = 'div class="col-sm-12 control-label"';
29 | $this->wrappers['control']['description'] = 'div class="form-text text-muted"';
30 | $this->wrappers['control']['errorcontainer'] = 'div class=note';
31 | $this->wrappers['error']['container'] = 'section';
32 | $this->wrappers['error']['item'] = 'p class="alert alert-danger alert-form"';
33 | }
34 |
35 | public function renderBegin(): string
36 | {
37 | $this->controlsInit();
38 | return parent::renderBegin();
39 | }
40 |
41 | public function renderEnd(): string
42 | {
43 | $this->controlsInit();
44 | return parent::renderEnd();
45 | }
46 |
47 | public function renderBody(): string
48 | {
49 | $this->controlsInit();
50 | return parent::renderBody();
51 | }
52 |
53 | public function renderControls($parent): string
54 | {
55 | $this->controlsInit();
56 | return parent::renderControls($parent);
57 | }
58 |
59 | public function renderPair(Control $control): string
60 | {
61 | $this->controlsInit();
62 |
63 | $pair = $this->getWrapper('pair container');
64 |
65 | if ($control instanceof RadioList || $control instanceof CheckboxList) {
66 | $pair->addHtml(Html::el('label', ['class' => 'col-12'])->addText($control->getLabel()->getText()));
67 | $pair->addHtml($this->generateControls($control));
68 |
69 | } elseif ($control instanceof Checkbox) {
70 | $pair->addHtml($this->generateControls($control));
71 |
72 | } elseif ($control instanceof UploadControl) {
73 | $pair->addHtml(Html::el('label', ['class' => 'col-12'])->addText($control->getLabel()->getText()));
74 | $pair->addHtml($this->generateControls($control));
75 |
76 | } else {
77 | $pair->addHtml($this->renderLabel($control));
78 | $pair->addHtml($this->renderControl($control));
79 | }
80 |
81 | $pair->setAttribute('id', $control->getName() . '-container');
82 | $pair->class($this->getValue($control->isRequired() ? 'pair .required' : 'pair .optional'), true);
83 | $pair->class($control->hasErrors() ? $this->getValue('pair .error') : null, true);
84 | $pair->class($control->getOption('class'), true);
85 | if (++$this->counter % 2) {
86 | $pair->class($this->getValue('pair .odd'), true);
87 | }
88 |
89 | return $pair->render(0);
90 | }
91 |
92 | protected function generateControls(BaseControl $control): Html
93 | {
94 | $wrapper = Html::el('div', ['class' => 'col-12']);
95 |
96 | if ($control instanceof RadioList || $control instanceof CheckboxList) {
97 | foreach ($control->getItems() as $key => $labelTitle) {
98 | if ($control instanceof RadioList) {
99 | $container = Html::el('div', ['class' => 'custom-control custom-radio custom-control-inline mb-5']);
100 | } else {
101 | $container = Html::el('div', ['class' => 'custom-control custom-checkbox custom-control-inline mb-5']);
102 | }
103 |
104 | $input = $control->getControlPart($key);
105 | $label = $control->getLabelPart($key);
106 |
107 | $label->setAttribute('class', 'custom-control-label');
108 | $input->setAttribute('class', 'custom-control-input');
109 |
110 | $container->addHtml($input);
111 | $container->addHtml($label);
112 |
113 | $wrapper->addHtml($container);
114 | }
115 | } elseif ($control instanceof Checkbox) {
116 | $container = Html::el('div', ['class' => 'custom-control custom-checkbox custom-control-inline']);
117 |
118 | $input = $control->getControlPart();
119 | $input->setAttribute('class', 'custom-control-input');
120 |
121 | $label = Html::el('label', ['class' => 'custom-control-label', 'for' => $control->getHtmlId()]);
122 | $label->addText($control->getCaption());
123 |
124 | $container->addHtml($input);
125 | $container->addHtml($label);
126 |
127 | $wrapper->addHtml($container);
128 |
129 | } elseif ($control instanceof UploadControl) {
130 | $container = Html::el('div', ['class' => 'custom-file']);
131 |
132 | $input = $control->getControlPart();
133 | $input->setAttribute('class', 'custom-file-input js-custom-file-input-enabled');
134 |
135 | $label = Html::el('label', ['class' => 'custom-file-label', 'for' => $control->getHtmlId()]);
136 |
137 | if ($control->getControl()->multiple) {
138 | $label->addText('Choose files');
139 | } else {
140 | $label->addText('Choose file');
141 | }
142 |
143 | $container->addHtml($input);
144 | $container->addHtml($label);
145 |
146 | $wrapper->addHtml($container);
147 | }
148 |
149 | return $wrapper;
150 | }
151 |
152 | public function renderPairMulti(array $controls): string
153 | {
154 | $this->controlsInit();
155 | return parent::renderPairMulti($controls);
156 | }
157 |
158 | public function renderLabel(Control $control): Html
159 | {
160 | $this->controlsInit();
161 | return parent::renderLabel($control);
162 | }
163 |
164 | public function renderControl(Control $control): Html
165 | {
166 | $this->controlsInit();
167 | return parent::renderControl($control);
168 | }
169 |
170 | protected function controlsInit(): void
171 | {
172 | if ($this->controlsInit) {
173 | return;
174 | }
175 |
176 | $this->controlsInit = true;
177 | $this->form->getElementPrototype()->appendAttribute('class', 'form-horizontal');
178 |
179 | foreach ($this->form->getControls() as $control) {
180 | $type = $control->getOption('type');
181 | if (in_array($type, ['text', 'textarea', 'select'], true)) {
182 | $control->getControlPrototype()->addClass('form-control');
183 |
184 | } elseif (in_array($type, ['checkbox', 'radio'], true)) {
185 | $control->getSeparatorPrototype()->setName('div')->addClass($type);
186 |
187 | } elseif ($control instanceof Controls\SubmitButton) {
188 | $class = empty($usedPrimary) ? 'btn btn-primary btn-block' : 'btn btn-default btn-block';
189 | $control->getControlPrototype()->setAttribute('class', $class);
190 | $usedPrimary = true;
191 |
192 | } elseif ($control instanceof Controls\TextBase) {
193 | $control->getControlPrototype()->appendAttribute('class', 'form-control');
194 |
195 | } elseif ($control instanceof DateInput) {
196 | $control->getControlPrototype()->appendAttribute('class', 'form-control form-control-date');
197 |
198 | } elseif ($control instanceof Controls\Checkbox) {
199 | $control->getControlPrototype()->appendAttribute('class', 'switch');
200 | $control->getControlPart()->appendAttribute('class', 'default');
201 | }
202 |
203 | if ($control instanceof Controls\SelectBox || $control instanceof Controls\MultiSelectBox) {
204 | $control->getControlPrototype()->appendAttribute('class', 'select2');
205 | }
206 | }
207 |
208 | foreach ($this->form->getControls() as $control) {
209 | $type = $control->getOption('type');
210 |
211 | if (in_array($type, ['text', 'textarea', 'select'], true)) {
212 | $control->getControlPrototype()->appendAttribute('class', 'form-control');
213 | }
214 | }
215 | }
216 | }
217 |
--------------------------------------------------------------------------------