This commit is contained in:
Simon Vieille 2022-03-13 19:32:32 +01:00
commit 6d0afb50b0
282 changed files with 19937 additions and 0 deletions

117
CHANGELOG.md Normal file
View File

@ -0,0 +1,117 @@
## [Unreleased]
### Added
### Fixed
### Changed
## [1.9.0] - 2022-03-13
### Added
* add murph version in admin ui
### Changed
* the core is now installed with composer
## [1.8.0] - 2022-03-10
### Added
* add security roles in app configuration
* add option to restrict node access to specific roles
### Changed
* rename `core/EventSuscriber` with `core/EventSubscriber`
## [1.7.3] - 2022-03-06
### Added
* add ability to rename file in the file manager
### Fixed
* fix user factory
* fix user creation from ui
## [1.7.2] - 2022-03-03
### Added
* add templates to render sections and items in the admin menu
### Fixed
* fix the analytic table when a path is a long
## [1.7.1] - 2022-03-01
### Added
* add translations
### Fixed
* fix missing directories
## [1.7.0] - 2022-03-01
### Fixed
* fix the analytic referers table when a referer has a long domain
### Changed
* upgrade dependencies
* move assets to the core directory
## [1.6.0] - 2022-02-28
### Added
* add block in field templates to allow override
* merge route params in crud admin redirects
* improve murph:user:create command
### Fixed
* fix form namespace prefix in the crud controller maker
* fix date field when the value is empty
* fix crud batch column width
* fix sidebar icon width
* fix cache clear task
### Changed
* remove password generation from the user factory
## [1.5.0] - 2022-02-25
### Added
* add desktop views and mobile views
### Changed
* upgrade dependencies
* replace jaybizzle/crawler-detect with matomo/device-detector
## [1.4.1] - 2022-02-23
### Added
* handle app urls in twig routing filters
### Fixed
* fix views in analytics modal
* replace empty path with "/" in analytics
### Changed
* update default templates
## [1.4.0] - 2022-02-21
### Added
* add basic analytics
## [1.3.0] - 2022-02-19
### Added
* add support of regexp with substitution in redirect
* url tags can be used as redirect location
* add builders to replace file information tags and url tags
### Fixed
* fix filemanager sorting
* fix batch action setter
## [1.2.0] - 2022-02-14
### Added
* add sort in file manager
* add redirect manager
### Changed
* replace node-sass with sass
## [1.1.0] - 2022-02-29
### Added
* add directory upload in file manager
### Fixed
* fix admin node routing
### Changed
* symfony/swiftmailer-bundle is replaced by symfony/mailer
## [1.0.1] - 2022-02-25
### Fixed
* fix Makefile environment vars (renaming)
* fix composer minimum stability
## [1.0.0] - 2022-01-23

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# Murph
Murph is an **open-source CMF** built on top of Symfony that helps you to **build your own CMS with several domains and languages**. It comes with:
* A fully implemented and customizable **tree manager** 🌳
* A **CRUD generator** ✏️
* A global **settings manager** and a navigation settings manager ⚙️
* A **tasks manager** 🧹
* A basic **web analytics** 📊
* **2FA authentication** 🔒
**Symfony developers will love build on Murph 🧪**
**End users will be fond of the interface and the powerful tools 💜**
📗 [Read the documentation](https://doc.murph-project.org/)

64
UPGRADE.md Normal file
View File

@ -0,0 +1,64 @@
## [Unreleased]
## Upgrade to v1.8.0
### Commands
```
make doctrine-migration
```
### Files
Event subscribers in `src/EventSubscriber` must update namespaces.
## Upgrade to v1.7.0
### Commands
```
yarn add sortablejs@^1.14.0
```
### Files
* `assets/css/_admin_extend.scss` is removed
* `assets/css/_admin_vars.scss` is removed
* `assets/css/_admin_vars.scss` is changed
* `assets/js/admin` is removed
* `assets/js/admin.js` is changed
## Upgrade to v1.5.0
### Commands
```
composer remove jaybizzle/crawler-detect
composer require matomo/device-detector
make doctrine-migration
```
## Upgrade to v1.4.0
### Commands
```
yarn remove node-sass
yarn add sass --dev --save
yarn add chart.js --save
composer require jaybizzle/crawler-detect
make doctrine-migration
make asset
```
### Configuration
```
// config/services.yaml
services:
App\Core\EventListener\RedirectListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
App\Core\EventListener\AnalyticListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
```

69
composer.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "murph/murph-core",
"description": "A powerful CMS framework",
"type": "project",
"license": "MIT",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.0.0",
"ext-ctype": "*",
"ext-iconv": "*",
"bjeavons/zxcvbn-php": "^1.3",
"cocur/slugify": "^4.1",
"composer/package-versions-deprecated": "1.11.99.1",
"doctrine/annotations": "^1.0",
"doctrine/doctrine-bundle": "^2.5",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.11",
"friendsofsymfony/jsrouting-bundle": "^2.8",
"jaybizzle/crawler-detect": "^1.2",
"knplabs/doctrine-behaviors": "^2.6",
"knplabs/knp-paginator-bundle": "^5.8",
"liip/imagine-bundle": "^2.7",
"matomo/device-detector": "^5.0",
"phpdocumentor/reflection-docblock": "^5.3",
"scheb/2fa-google-authenticator": "^5.13",
"scheb/2fa-qr-code": "^5.13",
"sensio/framework-extra-bundle": "^6.2",
"sensiolabs/ansi-to-html": "^1.2",
"spe/filesize-extension-bundle": "~2.0.0",
"stof/doctrine-extensions-bundle": "^1.7",
"symfony/apache-pack": "^1.0",
"symfony/asset": "5.4.*",
"symfony/console": "5.4.*",
"symfony/dotenv": "5.4.*",
"symfony/event-dispatcher": "5.4.*",
"symfony/expression-language": "5.4.*",
"symfony/finder": "5.4.*",
"symfony/flex": "^1.3.1",
"symfony/form": "5.4.*",
"symfony/framework-bundle": "5.4.*",
"symfony/http-client": "5.4.*",
"symfony/intl": "5.4.*",
"symfony/mailer": "5.4.*",
"symfony/mime": "5.4.*",
"symfony/monolog-bundle": "^3.1",
"symfony/notifier": "5.4.*",
"symfony/process": "5.4.*",
"symfony/property-access": "5.4.*",
"symfony/property-info": "5.4.*",
"symfony/proxy-manager-bridge": "5.4.*",
"symfony/security-bundle": "5.4.*",
"symfony/serializer": "5.4.*",
"symfony/string": "5.4.*",
"symfony/translation": "5.4.*",
"symfony/twig-bundle": "^5.2",
"symfony/validator": "5.4.*",
"symfony/web-link": "5.4.*",
"symfony/webpack-encore-bundle": "^1.11",
"symfony/yaml": "5.4.*",
"twig/extra-bundle": "^2.12|^3.3",
"twig/twig": "^2.12|^3.3"
},
"autoload": {
"psr-4": {
"App\\Core\\": "src/core/"
}
}
}

View File

@ -0,0 +1,204 @@
<?php
namespace App\Core\Analytic;
use App\Core\Entity\Site\Node;
use App\Core\Repository\Analytic\RefererRepositoryQuery;
use App\Core\Repository\Analytic\ViewRepositoryQuery;
/**
* class DateRangeAnalytic.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class DateRangeAnalytic
{
protected ViewRepositoryQuery $viewQuery;
protected RefererRepositoryQuery $refererQuery;
protected ?Node $node;
protected ?\DateTime $from;
protected ?\DateTime $to;
protected bool $reload = true;
protected array $cache = [];
public function __construct(ViewRepositoryQuery $viewQuery, RefererRepositoryQuery $refererQuery)
{
$this->viewQuery = $viewQuery;
$this->refererQuery = $refererQuery;
}
public function getViews(): array
{
$entities = $this->getEntities('view');
$this->reload = false;
if ($entities) {
$first = $entities[0];
$last = $entities[count($entities) - 1];
$diff = $first->getDate()->diff($last->getDate());
if ($diff->days >= 90) {
$format = 'Y-m';
} else {
$format = 'Y-m-d';
}
}
$datas = [];
foreach ($entities as $entity) {
$index = $entity->getDate()->format($format);
if (!isset($datas[$index])) {
$datas[$index] = 0;
}
$datas[$index] += $entity->getViews();
}
return $datas;
}
public function getPathViews(): array
{
$entities = $this->getEntities('view');
$this->reload = false;
$datas = [];
foreach ($entities as $entity) {
$index = $entity->getPath();
if (!isset($datas[$index])) {
$datas[$index] = [
'views' => 0,
'desktopViews' => 0,
'mobileViews' => 0,
];
}
$datas[$index]['views'] += $entity->getViews();
$datas[$index]['desktopViews'] += $entity->getDesktopViews();
$datas[$index]['mobileViews'] += $entity->getMobileViews();
}
uasort($datas, function($a, $b) {
if ($a['views'] > $b['views']) {
return -1;
}
if ($a['views'] < $b['views']) {
return 1;
}
return 0;
});
return $datas;
}
public function getReferers(): array
{
$entities = $this->getEntities('referer');
$this->reload = false;
$datas = [];
foreach ($entities as $entity) {
$index = parse_url($entity->getUri(), PHP_URL_HOST);
if (!isset($datas[$index])) {
$datas[$index] = [
'views' => 0,
'uris' => [],
];
}
$datas[$index]['views'] += $entity->getViews();
$path = parse_url($entity->getUri(), PHP_URL_PATH);
if (empty($path)) {
$path = '/';
}
if (!isset($datas[$index]['uris'][$path])) {
$datas[$index]['uris'][$path] = 0;
}
$datas[$index]['uris'][$path] += $entity->getViews();
}
uasort($datas, function($a, $b) {
if ($a['views'] > $b['views']) {
return -1;
}
if ($a['views'] < $b['views']) {
return 1;
}
return 0;
});
return $datas;
}
public function setDateRange(?\DateTime $from, ?\DateTime $to): self
{
$this->from = $from;
$this->to = $to;
$this->reload = true;
return $this;
}
public function setNode(?Node $node): self
{
$this->node = $node;
$this->reload = true;
return $this;
}
protected function getEntities(string $type): array
{
if ('view' === $type) {
$query = $this->viewQuery->create();
} elseif ('referer' === $type) {
$query = $this->refererQuery->create();
} else {
throw new \InvalidArgumentException('Invalid type');
}
if (!$this->reload && isset($this->cache[$type])) {
return $this->cache[$type];
}
if (null !== $this->from) {
$query
->andWhere('.date >= :from')
->setParameter(':from', $this->from)
;
}
if (null !== $this->to) {
$query
->andWhere('.date <= :to')
->setParameter(':to', $this->to)
;
}
if (null !== $this->node) {
$query
->andWhere('.node = :node')
->setParameter(':node', $this->node->getId())
;
}
$this->cache[$type] = $query->orderBy('.date')->find();
return $this->cache[$type];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Core\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* class UrlGenerator.
*
* @author Simon Vieille <simon@deblan.fr>
* @Annotation
*/
class UrlGenerator
{
public string $service;
public string $method;
public array $options = [];
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Core\Authenticator;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private EntityManagerInterface $entityManager;
private UrlGeneratorInterface $urlGenerator;
private CsrfTokenManagerInterface $csrfTokenManager;
private UserPasswordEncoderInterface $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'auth_login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(Security::LAST_USERNAME, $credentials['email']);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('admin_dashboard_index'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('auth_login');
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Core\Bundle;
use App\Core\DependencyInjection\CoreExtension;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
class CoreBundle extends Bundle
{
public function getContainerExtension(): ?ExtensionInterface
{
return new CoreExtension();
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Core\Cache;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpClient\Exception\TransportException;
/**
* class SymfonyCacheManager.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class SymfonyCacheManager
{
protected KernelInterface $kernel;
protected HttpClientInterface $httpClient;
protected UrlGeneratorInterface $urlGenerator;
public function __construct(KernelInterface $kernel, HttpClientInterface $httpClient, UrlGeneratorInterface $urlGenerator)
{
$this->kernel = $kernel;
$this->httpClient = $httpClient;
$this->urlGenerator = $urlGenerator;
}
public function cleanRouting()
{
$finder = new Finder();
$finder
->in($this->kernel->getCacheDir())
->depth('== 0')
->name('url_*.php*')
;
$pingUrl = $this->urlGenerator->generate('_ping', [], UrlGeneratorInterface::ABSOLUTE_URL);
foreach ($finder as $file) {
unlink((string) $file->getPathname());
}
try {
// Hack: used to regenerate cache of url generator
$this->httpClient->request('POST', $pingUrl);
} catch (ClientException $e) {
} catch (TransportException $e) {
}
}
public function cleanAll(OutputInterface $output = null)
{
$application = new Application($this->kernel);
$application->setAutoExit(false);
if (null === $output) {
$output = new BufferedOutput();
}
$input = new ArrayInput([
'command' => 'cache:warmup',
'-e' => $this->kernel->getEnvironment(),
]);
$application->run($input, $output);
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace App\Core\Command;
use App\Core\Factory\UserFactory;
use App\Core\Manager\EntityManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
class UserCreateCommand extends Command
{
protected static $defaultName = 'murph:user:create';
protected static $defaultDescription = 'Creates a user';
protected UserFactory $userFactory;
protected EntityManager $entityManager;
protected TokenGeneratorInterface $tokenGenerator;
public function __construct(
UserFactory $userFactory,
EntityManager $entityManager,
TokenGeneratorInterface $tokenGenerator
) {
$this->userFactory = $userFactory;
$this->entityManager = $entityManager;
$this->tokenGenerator = $tokenGenerator;
parent::__construct();
}
protected function configure()
{
$this
->setDescription(self::$defaultDescription)
->addArgument('email', InputArgument::OPTIONAL, 'E-mail')
->addOption('is-admin', null, InputOption::VALUE_NONE, 'Add the admin role')
->addOption('is-writer', null, InputOption::VALUE_NONE, 'Add the write role')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$helper = $this->getHelper('question');
$emailQuestion = new Question('E-mail: ');
$emailQuestion->setValidator(function ($value) {
if (empty($value)) {
throw new \RuntimeException('The email must not be empty.');
}
return $value;
});
$passwordQuestion = new Question('Password (leave empty to generate a random password): ');
$passwordQuestion->setHidden(true);
$isAdminDefault = $input->getOption('is-admin');
$isWriterDefault = $input->getOption('is-writer');
$isAdminQuestionLabel = sprintf('Administrator [%s] ', $isAdminDefault ? 'Y/n' : 'y/N');
$isWriterQuestionLabel = sprintf('Writer [%s] ', $isWriterDefault ? 'Y/n' : 'y/N');
$isAdminQuestion = new ConfirmationQuestion($isAdminQuestionLabel, $isAdminDefault);
$isWriterQuestion = new ConfirmationQuestion($isWriterQuestionLabel, $isWriterDefault);
$io->section('Authentication');
$email = $input->getArgument('email');
if (empty($email)) {
$email = $helper->ask($input, $output, $emailQuestion);
}
$password = $helper->ask($input, $output, $passwordQuestion);
$showPassword = empty($password);
if ($showPassword) {
$password = mb_substr($this->tokenGenerator->generateToken(), 0, 18);
$io->info(sprintf('Password: %s', $password));
} else {
$io->newLine();
}
$io->section('Roles');
$isAdmin = $helper->ask($input, $output, $isAdminQuestion);
$isWriter = $helper->ask($input, $output, $isWriterQuestion);
$user = $this->userFactory->create($email, $password);
$user->setIsAdmin($isAdmin);
$user->setIsWriter($isWriter);
$this->entityManager->create($user);
$io->newLine();
$io->success('User created!');
return Command::SUCCESS;
}
}

0
src/core/Controller/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,150 @@
<?php
namespace App\Core\Controller\Account;
use App\Core\Controller\Admin\AdminController;
use App\Core\Manager\EntityManager;
use App\Repository\UserRepository;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface as TotpAuthenticatorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use ZxcvbnPhp\Zxcvbn;
/**
* @Route("/admin/account")
*/
class AccountAdminController extends AdminController
{
/**
* @Route("/", name="admin_account")
*/
public function account(Request $request, TotpAuthenticatorInterface $totpAuthenticatorService): Response
{
$account = $this->getUser();
return $this->render('@Core/account/admin/edit.html.twig', [
'account' => $account,
]);
}
/**
* @Route("/2fa", name="admin_account_2fa")
*/
public function twoFactorAuthentication(
Request $request,
GoogleAuthenticatorInterface $totpAuthenticatorService,
EntityManager $entityManager
): Response {
if ($request->isMethod('GET')) {
return $this->redirectToRoute('admin_account');
}
$account = $this->getUser();
$csrfToken = $request->request->get('_csrf_token');
$enable = (bool) $request->request->get('enable');
$code = $request->request->get('code', '');
$secret = $request->request->get('secret', '');
$qrCodeContent = null;
if ($this->isCsrfTokenValid('2fa', $csrfToken)) {
if ($enable && !$account->isTotpAuthenticationEnabled()) {
if (empty($secret)) {
$secret = $totpAuthenticatorService->generateSecret();
$account->setTotpSecret($secret);
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
} else {
$account->setTotpSecret($secret);
$qrCodeContent = $totpAuthenticatorService->getQRContent($account);
if (!$totpAuthenticatorService->checkCode($account, $code)) {
$this->addFlash('error', 'The code is not valid.');
} else {
$this->addFlash('success', 'Double authentication enabled.');
$entityManager->update($account);
return $this->redirectToRoute('admin_account');
}
}
}
if (!$enable && $account->isTotpAuthenticationEnabled()) {
$account->setTotpSecret(null);
$entityManager->update($account);
$this->addFlash('success', 'Double authentication disabled.');
return $this->redirectToRoute('admin_account');
}
}
return $this->render('@Core/account/admin/edit.html.twig', [
'account' => $account,
'twoFaKey' => $secret,
'twoFaQrCodeContent' => $qrCodeContent,
]);
}
/**
* @Route("/password", name="admin_account_password", methods={"POST"})
*/
public function password(
Request $request,
UserRepository $repository,
TokenGeneratorInterface $tokenGenerator,
UserPasswordEncoderInterface $encoder,
EntityManager $entityManager
): Response {
$account = $this->getUser();
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('password', $csrfToken)) {
$password = $request->request->get('password');
if (!$encoder->isPasswordValid($account, $password)) {
$this->addFlash('error', 'The form is not valid.');
return $this->redirectToRoute('admin_account');
}
$password1 = $request->request->get('password1');
$password2 = $request->request->get('password2');
$zxcvbn = new Zxcvbn();
$strength = $zxcvbn->passwordStrength($password1, []);
if (4 === $strength['score'] && $password1 === $password2) {
$account
->setPassword($encoder->encodePassword($account, $password1))
->setConfirmationToken($tokenGenerator->generateToken())
;
$entityManager->update($account);
$this->addFlash('success', 'Password updated.');
return $this->redirectToRoute('admin_account');
}
}
$this->addFlash('error', 'The form is not valid.');
return $this->redirectToRoute('admin_account');
}
/**
* {@inheritdoc}
*/
protected function getSection(): string
{
return 'account';
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Core\Controller\Admin;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
abstract class AdminController extends AbstractController
{
protected array $coreParameters;
public function __construct(ParameterBagInterface $parameters)
{
$this->coreParameters = $parameters->get('core');
}
/**
* @Route("/_ping", name="_ping")
*/
public function ping()
{
return $this->json(true);
}
/**
* {@inheritdoc}
*/
protected function render(string $view, array $parameters = [], Response $response = null): Response
{
$parameters['section'] = $this->getSection();
$parameters['site_name'] = $this->coreParameters['site']['name'];
$parameters['site_logo'] = $this->coreParameters['site']['logo'];
$parameters['murph_version'] = defined('MURPH_VERSION') ? MURPH_VERSION : null;
return parent::render($view, $parameters, $response);
}
abstract protected function getSection(): string;
}

View File

@ -0,0 +1,342 @@
<?php
namespace App\Core\Controller\Admin\Crud;
use App\Core\Controller\Admin\AdminController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Entity\EntityInterface;
use App\Core\Manager\EntityManager;
use App\Core\Repository\RepositoryQuery;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
/**
* class CrudController.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class CrudController extends AdminController
{
protected array $filters = [];
protected array $sort = [
'label' => null,
'direction' => null,
];
abstract protected function getConfiguration(): CrudConfiguration;
protected function doIndex(int $page = 1, RepositoryQuery $query, Request $request, Session $session): Response
{
$configuration = $this->getConfiguration();
$this->applySort('index', $query, $request);
$this->updateFilters($request, $session);
$pager = $query
->usefilters($this->filters)
->paginate($page, $configuration->getmaxperpage('index'))
;
return $this->render($this->getConfiguration()->getView('index'), [
'configuration' => $configuration,
'pager' => $pager,
'sort' => $this->sort,
'filters' => [
'show' => null !== $configuration->getForm('filter'),
'isEmpty' => empty($this->filters),
],
]);
}
protected function doNew(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeCreate = null): Response
{
$configuration = $this->getConfiguration();
$this->prepareEntity($entity);
$form = $this->createForm($configuration->getForm('new'), $entity, $configuration->getFormOptions('new'));
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
if (null !== $beforeCreate) {
call_user_func_array($beforeCreate, [$entity, $form, $request]);
}
$entityManager->create($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge(
['entity' => $entity->getId()],
$configuration->getPageRouteParams('edit')
));
}
$this->addFlash('warning', 'The form is not valid.');
}
return $this->render($configuration->getView('new'), [
'form' => $form->createView(),
'configuration' => $configuration,
'entity' => $entity,
]);
}
protected function doShow(EntityInterface $entity): Response
{
$configuration = $this->getConfiguration();
return $this->render($configuration->getView('show'), [
'entity' => $entity,
'configuration' => $configuration,
]);
}
protected function doEdit(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeUpdate = null): Response
{
$configuration = $this->getConfiguration();
$this->prepareEntity($entity);
$form = $this->createForm($configuration->getForm('edit'), $entity, $configuration->getFormOptions('edit'));
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
if (null !== $beforeUpdate) {
call_user_func_array($beforeUpdate, [$entity, $form, $request]);
}
$entityManager->update($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge(
['entity' => $entity->getId()],
$configuration->getPageRouteParams('edit')
));
}
$this->addFlash('warning', 'The form is not valid.');
}
return $this->render($configuration->getView('edit'), [
'form' => $form->createView(),
'configuration' => $configuration,
'entity' => $entity,
]);
}
protected function doSort(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
{
$configuration = $this->getConfiguration();
$context = $request->query->get('context', 'index');
if (!$configuration->getIsSortableCollection($context)) {
throw $this->createNotFoundException();
}
$this->applySort($context, $query, $request);
$this->updateFilters($request, $session);
$pager = $query
->useFilters($this->filters)
->paginate($page, $configuration->getMaxPerPage($context))
;
if ($this->isCsrfTokenValid('sort', $request->query->get('_token'))) {
$items = $request->request->get('items', []);
$setter = 'set'.$configuration->getSortableCollectionProperty();
$orderStart = ($page - 1) * $configuration->getMaxPerPage($context);
foreach ($pager as $key => $entity) {
if (isset($items[$key + 1])) {
$entity->{$setter}($items[$key + 1] + $orderStart);
$entityManager->update($entity);
}
}
$this->addFlash('success', 'The data has been saved.');
} else {
$this->addFlash('warning', 'The form is not valid.');
}
return $this->json([]);
}
protected function doBatch(int $page = 1, RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session): Response
{
$configuration = $this->getConfiguration();
$datas = $request->request->get('batch', []);
$context = $datas['context'] ?? 'index';
$target = $datas['target'] ?? null;
$action = $datas['action'] ?? null;
$token = $datas['_token'] ?? null;
$items = $datas['items'] ?? [];
$batchAction = $configuration->getBatchAction($context, $action);
if (empty($context) || empty($action) || empty($target)) {
return $this->json([]);
}
if (!$this->isCsrfTokenValid('batch', $token) || empty($batchAction)) {
$this->addFlash('warning', 'The form is not valid.');
return $this->json([]);
}
$callback = $batchAction['callback'];
$this->applySort($context, $query, $request);
$this->updateFilters($request, $session);
$query->useFilters($this->filters);
if ('selection' === $target) {
$isSelection = true;
$pager = $query->paginate($page, $configuration->getMaxPerPage($context));
} else {
$isSelection = false;
$pager = $query->find();
}
foreach ($pager as $key => $entity) {
if (($isSelection && isset($items[$key + 1])) || !$isSelection) {
$callback($entity, $entityManager);
}
}
$this->addFlash('success', 'Batch action done.');
return $this->json([]);
}
protected function doDelete(EntityInterface $entity, EntityManager $entityManager, Request $request, callable $beforeDelete = null): Response
{
$configuration = $this->getConfiguration();
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
if (null !== $beforeDelete) {
call_user_func($beforeDelete, $entity);
}
$entityManager->delete($entity);
$this->addFlash('success', 'The data has been removed.');
}
return $this->redirectToRoute($configuration->getPageRoute('index'));
}
protected function doFilter(Session $session): Response
{
$configuration = $this->getConfiguration();
$type = $configuration->getForm('filter');
if (null === $type) {
throw $this->createNotFoundException();
}
$form = $this->createForm($type);
$form->submit($session->get($form->getName(), []));
return $this->render($configuration->getView('filter'), [
'form' => $form->createView(),
'configuration' => $configuration,
]);
}
protected function updateFilters(Request $request, Session $session)
{
$configuration = $this->getConfiguration();
$type = $configuration->getForm('filter');
if (null === $type) {
return;
}
$form = $this->createForm($type);
if ($request->query->has($form->getName())) {
$filters = $request->query->get($form->getName());
if ('0' === $filters) {
$filters = [];
}
} elseif ($session->has($form->getName())) {
$filters = $session->get($form->getName());
} else {
$filters = [];
}
$form->submit($filters);
if (empty($filters)) {
$this->filters = $filters;
$session->set($form->getName(), $filters);
} elseif ($form->isValid()) {
$this->filters = $form->getData();
$session->set($form->getName(), $filters);
}
}
protected function prepareEntity(EntityInterface $entity)
{
$configuration = $this->getConfiguration();
if ($configuration->isI18n()) {
foreach ($configuration->getLocales() as $locale) {
$entity->addTranslation($entity->translate($locale, false));
}
}
}
protected function applySort(string $context, RepositoryQuery $query, Request $request)
{
$configuration = $this->getConfiguration();
if ($configuration->getIsSortableCollection($context)) {
$query->orderBy(sprintf('.%s', $configuration->getSortableCollectionProperty()));
return;
}
$defaultSort = $configuration->getDefaultSort($context);
$name = $request->query->get('_sort', $defaultSort['label'] ?? null);
$direction = strtolower($request->query->get('_sort_direction', $defaultSort['direction'] ?? 'asc'));
if (!in_array($direction, ['asc', 'desc'])) {
$direction = 'asc';
}
foreach ($configuration->getFields($context) as $label => $field) {
$sortOption = $field['options']['sort'] ?? null;
if (null === $sortOption) {
continue;
}
if ($sortOption[0] !== $name) {
continue;
}
$sorter = $sortOption[1];
if (is_string($sorter)) {
$query->orderBy($sorter, $direction);
} else {
call_user_func_array($sorter, [$query, $direction]);
}
$this->sort = [
'label' => $label,
'direction' => $direction,
];
return;
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Core\Controller\Analytic;
use App\Core\Analytic\DateRangeAnalytic;
use App\Core\Entity\Site\Node;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/analytic")
*/
class AnalyticController extends AbstractController
{
/**
* @Route("/stats/{node}/{range}", name="admin_analytic_stats")
*/
public function stats(Node $node, DateRangeAnalytic $analytic, string $range = '7days'): Response
{
if (!in_array($range, ['7days', '30days', '90days', '1year'])) {
throw $this->createNotFoundException();
}
$analytic
->setDateRange(new \DateTime('now - '.$range), new \DateTime())
->setNode($node)
;
return $this->render('@Core/analytic/stats.html.twig', [
'range' => $range,
'views' => $analytic->getViews(),
'pathViews' => $analytic->getPathViews(),
'referers' => $analytic->getReferers(),
'node' => $node,
]);
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace App\Core\Controller\Auth;
use App\Core\Event\Account\PasswordRequestEvent;
use App\Core\Manager\EntityManager;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use ZxcvbnPhp\Zxcvbn;
class AuthController extends AbstractController
{
protected array $coreParameters;
public function __construct(ParameterBagInterface $parameters)
{
$this->coreParameters = $parameters->get('core');
}
/**
* @Route("/login", name="auth_login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('admin_dashboard_index');
}
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('@Core/auth/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
'site_name' => $this->coreParameters['site']['name'],
'site_logo' => $this->coreParameters['site']['logo'],
]);
}
/**
* @Route("/resetting/request", name="auth_resetting_request")
*/
public function requestResetting(Request $request, UserRepository $repository, EventDispatcherInterface $eventDispatcher): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('admin_dashboard_index');
}
if ($request->isMethod('POST')) {
$csrfToken = $request->request->get('_csrf_token');
if (!$this->isCsrfTokenValid('resetting_request', $csrfToken)) {
throw $this->createAccessDeniedException();
}
$username = trim((string) $request->request->get('username'));
if (!$username) {
throw $this->createAccessDeniedException();
}
$account = $repository->findOneByEmail($username);
if ($account) {
$requestedAt = $account->getPasswordRequestedAt();
if (null === $requestedAt || $requestedAt->getTimestamp() < (time() - 3600 / 2)) {
$eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT);
}
}
}
return $this->render('@Core/auth/resetting_request.html.twig', [
'email_sent' => $request->isMethod('POST'),
'site_name' => $this->coreParameters['site']['name'],
'site_logo' => $this->coreParameters['site']['logo'],
]);
}
/**
* @Route("/resetting/update/{token}", name="auth_resetting_update")
*/
public function requestUpdate(
string $token,
Request $request,
UserRepository $repository,
TokenGeneratorInterface $tokenGenerator,
UserPasswordEncoderInterface $encoder,
EntityManager $entityManager
): Response {
if ($this->getUser()) {
return $this->redirectToRoute('admin_dashboard_index');
}
$account = $repository->findOneByConfirmationToken($token);
$passwordUpdated = false;
$expired = true;
if ($account) {
$requestedAt = $account->getPasswordRequestedAt();
$expired = (null === $requestedAt || ($requestedAt->getTimestamp() < (time() - 3600 * 2)));
}
if ($request->isMethod('POST') && !$expired) {
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('resetting_update', $csrfToken)) {
$password = $request->request->get('password');
$password2 = $request->request->get('password2');
$zxcvbn = new Zxcvbn();
$strength = $zxcvbn->passwordStrength($password, []);
if (4 === $strength['score'] && $password === $password2) {
$account
->setPassword($encoder->encodePassword(
$account,
$password
))
->setConfirmationToken($tokenGenerator->generateToken())
->setPasswordRequestedAt(new \DateTime('now'))
;
$entityManager->update($account);
$passwordUpdated = true;
}
}
}
return $this->render('@Core/auth/resetting_update.html.twig', [
'password_updated' => $passwordUpdated,
'token' => $token,
'expired' => $expired,
'site_name' => $this->coreParameters['site']['name'],
'site_logo' => $this->coreParameters['site']['logo'],
]);
}
/**
* @Route("/logout", name="auth_logout")
*/
public function logout()
{
throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Core\Controller\Dashboard;
use App\Core\Controller\Admin\AdminController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin")
*/
class DashboardAdminController extends AdminController
{
/**
* @Route("/", name="admin_dashboard_index")
*/
public function index(): Response
{
return $this->render('@Core/dashboard/index.html.twig', [
]);
}
protected function getSection(): string
{
return 'dashboard';
}
}

View File

@ -0,0 +1,438 @@
<?php
namespace App\Core\Controller\FileManager;
use App\Core\Controller\Admin\AdminController;
use App\Core\FileManager\FsFileManager;
use App\Core\Form\FileManager\DirectoryCreateType;
use App\Core\Form\FileManager\DirectoryRenameType;
use App\Core\Form\FileManager\FileInformationType;
use App\Core\Form\FileManager\FileRenameType;
use App\Core\Form\FileManager\FileUploadType;
use App\Core\Manager\EntityManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/admin/file_manager")
*/
class FileManagerAdminController extends AdminController
{
/**
* @Route("/", name="admin_file_manager_index")
*/
public function index(): Response
{
return $this->render('@Core/file_manager/index.html.twig');
}
/**
* @Route("/api/directory", name="admin_file_manager_api_directory", options={"expose"=true})
*/
public function directory(FsFileManager $manager, Request $request): Response
{
$options = [
'sort' => $request->query->get('_sort', 'name'),
'sort_direction' => $request->query->get('_sort_direction', 'asc'),
];
$files = $manager->list($request->query->get('directory', '/'), $options);
return $this->json($files);
}
/**
* @Route("/info/{tab}/{context}/{ajax}", name="admin_file_manager_info", options={"expose"=true})
*/
public function info(
FsFileManager $manager,
Request $request,
EntityManager $entityManager,
TranslatorInterface $translator,
string $context = 'crud',
string $tab = 'information',
bool $ajax = false
): Response {
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$splInfo) {
throw $this->createNotFoundException();
}
$fileInfo = $manager->getFileInformation($request->query->get('file'));
$path = $manager->getPathUri().'/'.$splInfo->getRelativePathname();
$form = $this->createForm(FileInformationType::class, $fileInfo);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($fileInfo);
if (!$request->isXmlHttpRequest()) {
$this->addFlash('success', 'The data has been saved.');
} else {
return $this->json([
'_error' => 0,
'_message' => $translator->trans('The data has been saved.'),
'_level' => 'success',
'_dispatch' => 'file_manager.info.update.success',
]);
}
} else {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('warning', 'The form is not valid.');
} else {
return $this->json([
'_error' => 1,
'_message' => $translator->trans('The form is not valid.'),
'_level' => 'warning',
'_dispatch' => 'file_manager.info.update.error',
]);
}
}
return $this->redirectToRoute('admin_file_manager_index', [
'data-modal' => $this->generateUrl('admin_file_manager_info', [
'file' => $request->query->get('file'),
'tab' => 'attributes',
]),
'path' => $splInfo->getRelativePath(),
]);
}
return $this->render('@Core/file_manager/info.html.twig', [
'splInfo' => $splInfo,
'path' => $path,
'isLocked' => $manager->isLocked($splInfo->getRelativePathname()),
'tab' => $tab,
'form' => $form->createView(),
'context' => $context,
'ajax' => $ajax,
]);
}
/**
* @Route("/directory/new/{ajax}", name="admin_file_manager_directory_new", options={"expose"=true}, methods={"GET", "POST"})
*/
public function directoryNew(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$splInfo) {
throw $this->createNotFoundException();
}
if (!$splInfo->isDir()) {
throw $this->createNotFoundException();
}
if ($manager->isLocked($request->query->get('file'))) {
return $this->render('@Core/file_manager/directory_new.html.twig', [
'locked' => true,
]);
}
$form = $this->createForm(DirectoryCreateType::class);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$status = $manager->createDirectory($form->get('name')->getData(), $request->query->get('file'));
if (true === $status) {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('success', 'Directory created.');
} else {
return $this->json([
'_error' => 0,
'_message' => $translator->trans('Directory created.'),
'_level' => 'success',
'_dispatch' => 'file_manager.directory.new.success',
]);
}
} else {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('warning', 'Directory not created.');
} else {
return $this->json([
'_error' => 1,
'_message' => $translator->trans('Directory not created.'),
'_level' => 'warning',
'_dispatch' => 'file_manager.directory.new.error',
]);
}
}
} else {
$this->addFlash('warning', 'Unauthorized char(s).');
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $splInfo->getRelativePathname(),
]);
}
return $this->render('@Core/file_manager/directory_new.html.twig', [
'form' => $form->createView(),
'file' => $request->query->get('file'),
'ajax' => $ajax,
'locked' => false,
]);
}
/**
* @Route("/directory/rename/{ajax}", name="admin_file_manager_directory_rename", methods={"GET", "POST"})
*/
public function directoryRename(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$splInfo) {
throw $this->createNotFoundException();
}
if (!$splInfo->isDir()) {
throw $this->createNotFoundException();
}
if ($manager->isLocked($request->query->get('file'))) {
return $this->render('@Core/file_manager/directory_rename.html.twig', [
'locked' => true,
]);
}
$form = $this->createForm(DirectoryRenameType::class, [
'name' => $splInfo->getFilename(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$status = $manager->renameDirectory($form->get('name')->getData(), $request->query->get('file'));
if (true === $status) {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('success', 'Directory renamed.');
} else {
return $this->json([
'_error' => 0,
'_message' => $translator->trans('Directory renamed.'),
'_level' => 'success',
'_dispatch' => 'file_manager.directory.rename.success',
]);
}
} else {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('warning', 'Directory not renamed.');
} else {
return $this->json([
'_error' => 1,
'_message' => $translator->trans('Directory not renamed.'),
'_level' => 'warning',
'_dispatch' => 'file_manager.directory.rename.error',
]);
}
}
} else {
$this->addFlash('warning', 'Unauthorized char(s).');
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $splInfo->getRelativePath(),
]);
}
return $this->render('@Core/file_manager/directory_rename.html.twig', [
'form' => $form->createView(),
'file' => $request->query->get('file'),
'locked' => false,
'ajax' => $ajax,
]);
}
/**
* @Route("/file/rename/{ajax}", name="admin_file_manager_file_rename", methods={"GET", "POST"})
*/
public function fileRename(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$splInfo) {
throw $this->createNotFoundException();
}
if ($splInfo->isDir()) {
throw $this->createNotFoundException();
}
if ($manager->isLocked($request->query->get('file'))) {
return $this->render('@Core/file_manager/file_rename.html.twig', [
'locked' => true,
]);
}
$form = $this->createForm(FileRenameType::class, [
'name' => preg_replace(sprintf('/\.%s/', $splInfo->getExtension()), '', $splInfo->getFilename()),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$status = $manager->renameFile($form->get('name')->getData(), $request->query->get('file'));
if (true === $status) {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('success', 'File renamed.');
} else {
return $this->json([
'_error' => 0,
'_message' => $translator->trans('File renamed.'),
'_level' => 'success',
'_dispatch' => 'file_manager.file.rename.success',
]);
}
} else {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('warning', 'File not renamed.');
} else {
return $this->json([
'_error' => 1,
'_message' => $translator->trans('File not renamed.'),
'_level' => 'warning',
'_dispatch' => 'file_manager.file.rename.error',
]);
}
}
} else {
$this->addFlash('warning', 'Unauthorized char(s).');
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $splInfo->getRelativePath(),
]);
}
return $this->render('@Core/file_manager/file_rename.html.twig', [
'form' => $form->createView(),
'file' => $request->query->get('file'),
'exention' => $splInfo->getExtension(),
'locked' => false,
'ajax' => $ajax,
]);
}
/**
* @Route("/upload/{ajax}", name="admin_file_manager_upload", options={"expose"=true}, methods={"GET", "POST"})
*/
public function upload(FsFileManager $manager, Request $request, TranslatorInterface $translator, bool $ajax = false): Response
{
$splInfo = $manager->getSplInfo($request->query->get('file'));
if (!$splInfo) {
throw $this->createNotFoundException();
}
if (!$splInfo->isDir()) {
throw $this->createAccessDeniedException();
}
if ($manager->isLocked($request->query->get('file'))) {
return $this->render('@Core/file_manager/upload.html.twig', [
'locked' => true,
]);
}
$form = $this->createForm(FileUploadType::class, null, [
'mimes' => $manager->getMimes(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
if ($form->get('files')->getData()) {
$manager->upload(
$form->get('files')->getData(),
$request->query->get('file')
);
}
if ($form->get('directory')->getData()) {
$manager->upload(
$form->get('directory')->getData(),
$request->query->get('file'),
$_FILES['file_upload']['full_path']['directory'] ?? []
);
}
if (!$request->isXmlHttpRequest()) {
$this->addFlash('success', 'Files uploaded.');
} else {
return $this->json([
'_error' => 0,
'_message' => $translator->trans('Files uploaded.'),
'_level' => 'success',
'_dispatch' => 'file_manager.file.new.success',
]);
}
} else {
if (!$request->isXmlHttpRequest()) {
$this->addFlash('warning', 'Unauthorized file type(s).');
} else {
return $this->json([
'_error' => 1,
'_message' => $translator->trans('Unauthorized file type(s).'),
'_level' => 'warning',
'_dispatch' => 'file_manager.file.new.error',
]);
}
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $splInfo->getRelativePathname(),
]);
}
return $this->render('@Core/file_manager/upload.html.twig', [
'form' => $form->createView(),
'file' => $request->query->get('file'),
'locked' => false,
'ajax' => $ajax,
]);
}
/**
* @Route("/delete", name="admin_file_manager_delete", methods={"DELETE"})
*/
public function delete(FsFileManager $manager, Request $request): Response
{
$path = $request->request->get('file');
$splInfo = $manager->getSplInfo($request->request->get('file'));
if (!$splInfo) {
throw $this->createNotFoundException();
}
if ($this->isCsrfTokenValid('delete', $request->request->get('_token'))) {
if ($manager->delete($path)) {
$this->addFlash('success', 'The data has been removed.');
} else {
$this->addFlash('warning', 'The data has not been removed.');
}
}
return $this->redirectToRoute('admin_file_manager_index', [
'path' => $splInfo->getRelativePath(),
]);
}
protected function getSection(): string
{
return 'file_manager';
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace App\Core\Controller\Redirect;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Redirect as Entity;
use App\Core\Factory\RedirectFactory as Factory;
use App\Core\Form\Filter\RedirectFilterType as FilterType;
use App\Core\Form\RedirectType as Type;
use App\Core\Manager\EntityManager;
use App\Core\Repository\RedirectRepositoryQuery as RepositoryQuery;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class RedirectAdminController extends CrudController
{
/**
* @Route("/admin/redirect/{page}", name="admin_redirect_index", methods={"GET"}, requirements={"page":"\d+"})
*/
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/redirect/new", name="admin_redirect_new", methods={"GET", "POST"})
*/
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
{
return $this->doNew($factory->create(), $entityManager, $request);
}
/**
* @Route("/admin/redirect/show/{entity}", name="admin_redirect_show", methods={"GET"})
*/
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
/**
* @Route("/admin/redirect/filter", name="admin_redirect_filter", methods={"GET"})
*/
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/redirect/edit/{entity}", name="admin_redirect_edit", methods={"GET", "POST"})
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/redirect/sort/{page}", name="admin_redirect_sort", methods={"POST"}, requirements={"page":"\d+"})
*/
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doSort($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/redirect/batch/{page}", name="admin_redirect_batch", methods={"POST"}, requirements={"page":"\d+"})
*/
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doBatch($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/redirect/delete/{entity}", name="admin_redirect_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
}
protected function getConfiguration(): CrudConfiguration
{
return CrudConfiguration::create()
->setPageTitle('index', 'Redirects')
->setPageTitle('edit', '{label}')
->setPageTitle('new', 'New redirect')
->setPageRoute('index', 'admin_redirect_index')
->setPageRoute('new', 'admin_redirect_new')
->setPageRoute('edit', 'admin_redirect_edit')
->setPageRoute('sort', 'admin_redirect_sort')
->setPageRoute('batch', 'admin_redirect_batch')
->setPageRoute('delete', 'admin_redirect_delete')
->setPageRoute('filter', 'admin_redirect_filter')
->setForm('edit', Type::class, [])
->setForm('new', Type::class)
->setForm('filter', FilterType::class)
->setView('form', '@Core/redirect/redirect_admin/_form.html.twig')
->setMaxPerPage('index', 100)
->setIsSortableCollection('index', true)
->setAction('index', 'show', false)
->setAction('edit', 'show', false)
->setField('index', 'Label', Field\TextField::class, [
'property' => 'label',
'attr' => ['class' => 'col-4'],
])
->setField('index', 'Rule', Field\TextField::class, [
'view' => '@Core/redirect/redirect_admin/field/rule.html.twig',
'attr' => ['class' => 'col-6'],
])
->setField('index', 'Enabled', Field\ButtonField::class, [
'property_builder' => function(EntityInterface $entity) {
return $entity->getIsEnabled() ? 'Yes' : 'No';
},
'attr' => ['class' => 'col-1'],
'button_attr_builder' => function(EntityInterface $entity) {
return ['class' => 'btn btn-sm btn-'.($entity->getIsEnabled() ? 'success' : 'primary')];
},
])
->setField('index', 'Type', Field\ButtonField::class, [
'property' => 'redirectCode',
'attr' => ['class' => 'col-1'],
'button_attr' => ['class' => 'btn btn-sm btn-light border-secondary font-weight-bold'],
])
->setBatchAction('index', 'enable', 'Enable', function (EntityInterface $entity, EntityManager $manager) {
$entity->setIsEnabled(true);
$manager->update($entity);
})
->setBatchAction('index', 'disable', 'Disable', function (EntityInterface $entity, EntityManager $manager) {
$entity->setIsEnabled(false);
$manager->update($entity);
})
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity);
})
;
}
protected function getSection(): string
{
return 'site_navigation';
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Core\Controller\Setting;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\NavigationSetting as Entity;
use App\Core\Event\Setting\NavigationSettingEvent;
use App\Core\Manager\EntityManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/navigation_setting")
*/
class NavigationSettingAdminController extends AdminController
{
/**
* @Route("/edit/{entity}", name="admin_navigation_setting_edit")
*/
public function edit(
Entity $entity,
EntityManager $entityManager,
EventDispatcherInterface $eventDispatcher,
Request $request
): Response {
$builder = $this->createFormBuilder($entity);
$event = new NavigationSettingEvent([
'builder' => $builder,
'entity' => $entity,
'options' => [],
]);
$eventDispatcher->dispatch($event, NavigationSettingEvent::FORM_INIT_EVENT);
$form = $builder->getForm();
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirectToRoute('admin_site_navigation_show', [
'entity' => $entity->getNavigation()->getId(),
]);
}
$this->addFlash('warning', 'The form is not valid.');
}
return $this->render('@Core/setting/navigation_setting_admin/edit.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
'options' => $event->getData()['options'],
]);
}
/**
* @Route("/delete/{entity}", name="admin_navigation_setting_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'The data has been removed.');
}
return $this->redirectToRoute('admin_site_navigation_show', [
'entity' => $entity->getNavigation()->getId(),
]);
}
public function getSection(): string
{
return '';
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Core\Controller\Setting;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\Setting as Entity;
use App\Core\Event\Setting\SettingEvent;
use App\Core\Manager\EntityManager;
use App\Core\Repository\SettingRepositoryQuery as RepositoryQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/setting")
*/
class SettingAdminController extends AdminController
{
/**
* @Route("/{page}", name="admin_setting_index", requirements={"page": "\d+"})
*/
public function index(
RepositoryQuery $query,
EventDispatcherInterface $eventDispatcher,
Request $request,
int $page = 1
): Response {
$eventDispatcher->dispatch(new SettingEvent(), SettingEvent::INIT_EVENT);
$pager = $query
->orderBy('.section, .label')
->paginate($page)
;
return $this->render('@Core/setting/setting_admin/index.html.twig', [
'pager' => $pager,
]);
}
/**
* @Route("/edit/{entity}", name="admin_setting_edit")
*/
public function edit(
Entity $entity,
EntityManager $entityManager,
EventDispatcherInterface $eventDispatcher,
Request $request
): Response {
$builder = $this->createFormBuilder($entity);
$event = new SettingEvent([
'builder' => $builder,
'entity' => $entity,
'options' => [],
]);
$eventDispatcher->dispatch($event, SettingEvent::FORM_INIT_EVENT);
$form = $builder->getForm();
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirectToRoute('admin_setting_index');
}
$this->addFlash('warning', 'The form is not valid.');
}
return $this->render('@Core/setting/setting_admin/edit.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
'options' => $event->getData()['options'],
]);
}
/**
* @Route("/delete/{entity}", name="admin_setting_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'The data has been removed.');
}
return $this->redirectToRoute('admin_setting_index');
}
public function getSection(): string
{
return 'setting';
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\Site\Menu as Entity;
use App\Core\Entity\Site\Navigation;
use App\Core\Factory\Site\MenuFactory as EntityFactory;
use App\Core\Form\Site\MenuType as EntityType;
use App\Core\Manager\EntityManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/menu")
*/
class MenuAdminController extends AdminController
{
/**
* @Route("/new/{navigation}", name="admin_site_menu_new", methods={"POST"})
*/
public function new(Navigation $navigation, EntityFactory $factory, EntityManager $entityManager, Request $request): Response
{
$entity = $factory->create($navigation);
$form = $this->createForm(EntityType::class, $entity);
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->create($entity);
$this->addFlash('success', 'The data has been saved.');
} else {
$this->addFlash('warning', 'The form is not valid.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $navigation->getId(),
]);
}
/**
* @Route("/edit/{entity}", name="admin_site_menu_edit", methods={"POST"})
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
$form = $this->createForm(EntityType::class, $entity);
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity);
$this->addFlash('success', 'The data has been saved.');
} else {
$this->addFlash('warning', 'The form is not valid.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getNavigation()->getId(),
]);
}
/**
* @Route("/delete/{entity}", name="admin_site_menu_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'The data has been removed.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getNavigation()->getId(),
]);
}
public function getSection(): string
{
return '';
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field;
use App\Core\Entity\Site\Navigation as Entity;
use App\Core\Event\Setting\NavigationSettingEvent;
use App\Core\Factory\Site\NavigationFactory as Factory;
use App\Core\Form\Site\NavigationType as Type;
use App\Core\Manager\EntityManager;
use App\Core\Repository\NavigationSettingRepositoryQuery;
use App\Core\Repository\Site\NavigationRepositoryQuery as RepositoryQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class NavigationAdminController extends CrudController
{
/**
* @Route("/admin/site/navigation/{page}", name="admin_site_navigation_index", methods={"GET"}, requirements={"page":"\d+"})
*/
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/site/navigation/new", name="admin_site_navigation_new", methods={"GET", "POST"})
*/
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
{
return $this->doNew($factory->create(), $entityManager, $request);
}
/**
* @Route("/admin/site/navigation/show/{entity}", name="admin_site_navigation_show", methods={"GET"})
*/
public function show(
Entity $entity,
EventDispatcherInterface $eventDispatcher,
NavigationSettingRepositoryQuery $settingQuery
): Response {
$eventDispatcher->dispatch(new NavigationSettingEvent([
'navigation' => $entity,
]), NavigationSettingEvent::INIT_EVENT);
$settings = $settingQuery
->where('.navigation = :navigation')
->orderBy('.section, .label')
->setParameter(':navigation', $entity->getId())
->paginate(1, 1000)
;
$this->getConfiguration()->addViewData('show', 'settings', $settings);
return $this->doShow($entity);
}
/**
* @Route("/admin/site/navigation/filter", name="admin_site_navigation_filter", methods={"GET"})
*/
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/site/navigation/edit/{entity}", name="admin_site_navigation_edit", methods={"GET", "POST"})
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/site/navigation/sort/{page}", name="admin_site_navigation_sort", methods={"POST"}, requirements={"page":"\d+"})
*/
public function sort(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1, ): Response
{
return $this->doSort($page, $query, $entityManager, $request, $session);
}
/**
* @Route("/admin/site/navigation/delete/{entity}", name="admin_site_navigation_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
}
protected function getConfiguration(): CrudConfiguration
{
return CrudConfiguration::create()
->setPageTitle('index', 'Navigations')
->setPageTitle('edit', '{label}')
->setPageTitle('new', 'New navigation')
->setPageTitle('show', '{label}')
->setPageRoute('index', 'admin_site_navigation_index')
->setPageRoute('new', 'admin_site_navigation_new')
->setPageRoute('edit', 'admin_site_navigation_edit')
->setPageRoute('show', 'admin_site_navigation_show')
->setPageRoute('sort', 'admin_site_navigation_sort')
->setPageRoute('delete', 'admin_site_navigation_delete')
->setPageRoute('filter', 'admin_site_navigation_filter')
->setPageRoute('redirects', 'admin_redirect_index')
->setForm('edit', Type::class, [])
->setForm('new', Type::class)
->setView('index', '@Core/site/navigation_admin/index.html.twig')
->setView('show', '@Core/site/navigation_admin/show.html.twig')
->setView('show_entity', '@Core/site/navigation_admin/_show.html.twig')
->setView('form', '@Core/site/navigation_admin/_form.html.twig')
->setIsSortableCollection('index', true)
->setField('index', 'Label', Field\TextField::class, [
'property' => 'label',
'attr' => ['class' => 'miw-200'],
])
->setField('index', 'Domain', Field\ButtonField::class, [
'property' => 'domain',
'button_attr' => ['class' => 'btn btn-light'],
'attr' => ['class' => 'miw-200'],
])
->setField('index', 'Locale', Field\ButtonField::class, [
'property' => 'locale',
'button_attr' => ['class' => 'btn btn-light'],
])
;
}
protected function getSection(): string
{
return 'site_navigation';
}
}

View File

@ -0,0 +1,310 @@
<?php
namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\Site\Node;
use App\Core\Entity\Site\Node as Entity;
use App\Core\Entity\Site\Page\Page;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\Factory\Site\NodeFactory as EntityFactory;
use App\Core\Factory\Site\Page\PageFactory;
use App\Core\Form\Site\NodeMoveType;
use App\Core\Form\Site\NodeType as EntityType;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;
use App\Core\Site\ControllerLocator;
use App\Core\Site\RoleLocator;
use App\Core\Site\PageLocator;
use App\Core\Sitemap\SitemapBuilder;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
/**
* @Route("/admin/site/node")
*/
class NodeAdminController extends AbstractController
{
/**
* @Route("/new/{node}", name="admin_site_node_new")
*/
public function new(
Node $node,
EntityFactory $factory,
PageFactory $pageFactory,
EntityManager $entityManager,
NodeRepository $nodeRepository,
PageLocator $pageLocator,
ControllerLocator $controllerLocator,
RoleLocator $roleLocator,
Request $request
): Response {
$entity = $factory->create($node->getMenu());
$form = $this->createForm(EntityType::class, $entity, [
'pages' => $pageLocator->getPages(),
'controllers' => $controllerLocator->getControllers(),
'roles' => $roleLocator->getRoles(),
'navigation' => $node->getMenu()->getNavigation(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$position = $form->get('position')->getData();
$parent = 'above' === $position ? $node : $node->getParent();
$entity->setParent($parent);
if ('above' === $position) {
$nodeRepository->persistAsLastChild($entity, $node);
} else {
if ('after' === $position) {
$nodeRepository->persistAsNextSiblingOf($entity, $node);
} elseif ('before' === $position) {
$nodeRepository->persistAsPrevSiblingOf($entity, $node);
}
}
$this->handlePageAssociation(
$form->get('pageAction')->getData(),
$form->get('pageEntity')->getData(),
$form->get('pageType')->getData(),
$entity,
$pageFactory,
$pageLocator
);
$entityManager->create($entity);
$this->addFlash('success', 'The data has been saved.');
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
'navigation' => $node->getMenu()->getNavigation()->getId(),
'data-modal' => $this->generateUrl('admin_site_node_edit', ['entity' => $entity->getId()]),
]).sprintf('#node-%d', $entity->getId()));
}
$this->addFlash('warning', 'The form is not valid.');
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]).sprintf('#node-%d', $entity->getId()));
}
return $this->render('@Core/site/node_admin/new.html.twig', [
'form' => $form->createView(),
'node' => $node,
'entity' => $entity,
'tab' => 'content',
]);
}
/**
* @Route("/edit/{entity}/{tab}", name="admin_site_node_edit")
*/
public function edit(
Entity $entity,
EntityManager $entityManager,
PageFactory $pageFactory,
PageLocator $pageLocator,
ControllerLocator $controllerLocator,
RoleLocator $roleLocator,
Request $request,
string $tab = 'content'
): Response {
$form = $this->createForm(EntityType::class, $entity, [
'pages' => $pageLocator->getPages(),
'controllers' => $controllerLocator->getControllers(),
'roles' => $roleLocator->getRoles(),
'navigation' => $entity->getMenu()->getNavigation(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$this->handlePageAssociation(
$form->get('pageAction')->getData(),
$form->get('pageEntity')->getData(),
$form->get('pageType')->getData(),
$entity,
$pageFactory,
$pageLocator
);
$entityManager->update($entity);
$this->addFlash('success', 'The data has been saved.');
} else {
$this->addFlash('warning', 'The form is not valid.');
}
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
'data-modal' => $this->generateUrl('admin_site_node_edit', ['entity' => $entity->getId()]),
]).sprintf('#node-%d', $entity->getId()));
}
$page = $entity->getPage();
if ($page !== null) {
$pageConfiguration = $pageLocator->getPages()[get_class($page)] ?? null;
} else {
$pageConfiguration = null;
}
return $this->render('@Core/site/node_admin/edit.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
'page' => $page,
'pageConfiguration' => $pageConfiguration,
'tab' => $tab,
]);
}
/**
* @Route("/urls/{entity}", name="admin_site_node_urls")
*/
public function urls(Entity $entity, SitemapBuilder $builder): Response
{
return $this->render('@Core/site/node_admin/urls.html.twig', [
'entity' => $entity,
'urls' => $builder->getNodeUrls($entity),
]);
}
/**
* @Route("/move/{entity}", name="admin_site_node_move")
*/
public function move(
Entity $entity,
EntityManager $entityManager,
NodeRepository $nodeRepository,
Request $request
): Response {
$form = $this->createForm(NodeMoveType::class, null, [
'menu' => $entity->getMenu(),
]);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->get('node')->getData()->getId() === $entity->getId()) {
$form->get('node')->addError(new FormError('Élement de référence invalide.'));
}
if ($form->isValid()) {
$position = $form->get('position')->getData();
$node = $form->get('node')->getData();
$parent = 'above' === $position ? $node : $node->getParent();
$entity->setParent($parent);
if ('above' === $position) {
$nodeRepository->persistAsLastChild($entity, $node);
$entityManager->flush();
} else {
if ('after' === $position) {
$nodeRepository->persistAsNextSiblingOf($entity, $node);
} elseif ('before' === $position) {
$nodeRepository->persistAsPrevSiblingOf($entity, $node);
}
$entityManager->flush();
}
$this->addFlash('success', 'The data has been saved.');
} else {
$this->addFlash('warning', 'The form is not valid.');
}
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]).sprintf('#node-%d', $entity->getId()));
}
return $this->render('@Core/site/node_admin/move.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/toggle/visibility/{entity}", name="admin_site_node_toggle_visibility", methods={"POST"})
*/
public function toggleVisibility(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('toggle_visibility'.$entity->getId(), $request->request->get('_token'))) {
$entity->setIsVisible(!$entity->getIsVisible());
$entityManager->update($entity);
$this->addFlash('success', 'The data has been saved.');
}
return $this->redirect($this->generateUrl('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]).sprintf('#node-%d', $entity->getId()));
}
/**
* @Route("/delete/{entity}", name="admin_site_node_delete", methods={"DELETE"})
*/
public function delete(
Entity $entity,
NodeRepository $nodeRepository,
EventDispatcherInterface $eventDispatcher,
Request $request
): Response {
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_DELETE_EVENT);
$nodeRepository->removeFromTree($entity);
$nodeRepository->reorder($entity->getMenu()->getRootNode());
$eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
$this->addFlash('success', 'Donnée supprimée.');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $entity->getMenu()->getNavigation()->getId(),
]);
}
protected function handlePageAssociation(
string $pageAction,
?Page $pageEntity,
string $pageType,
Entity $entity,
PageFactory $pageFactory,
PageLocator $pageLocator
) {
if ('new' === $pageAction) {
$pageConfiguration = $pageLocator->getPage($pageType);
$page = $pageFactory->create($pageType, $entity->getLabel());
$page->setTemplate($pageConfiguration->getTemplates()[0]['file']);
$entity
->setPage($page)
->setAliasNode(null)
;
} elseif ('existing' === $pageAction) {
if ($pageEntity) {
$entity->setPage($pageEntity);
} else {
$this->addFlash('info', 'Aucun changement de page effectué.');
}
$entity->setAliasNode(null);
} elseif ('alias' === $pageAction) {
$entity->setPage(null);
} elseif ('none' === $pageAction) {
$entity
->setPage(null)
->setAliasNode(null)
;
}
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field;
use App\Core\Entity\Site\Page\Page as Entity;
use App\Core\Form\Site\Page\Filter\PageFilterType as FilterType;
use App\Core\Form\Site\Page\PageType as Type;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\Page\PageRepositoryQuery as RepositoryQuery;
use App\Core\Site\PageLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use App\Core\Event\Page\PageEditEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use App\Core\Entity\EntityInterface;
class PageAdminController extends CrudController
{
/**
* @Route("/admin/site/page/{page}", name="admin_site_page_index", methods={"GET"}, requirements={"page":"\d+"})
*/
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/site/page/show/{entity}", name="admin_site_page_show", methods={"GET"})
*/
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
/**
* @Route("/admin/site/page/filter", name="admin_site_page_filter", methods={"GET"})
*/
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/site/page/edit/{entity}", name="admin_site_page_edit", methods={"GET", "POST"})
*/
public function edit(
int $entity,
EntityManager $entityManager,
RepositoryQuery $repositoryQuery,
PageLocator $pageLocator,
EventDispatcherInterface $eventDispatcher,
Request $request
): Response {
$entity = $repositoryQuery->filterById($entity)->findOne();
$event = new PageEditEvent($entity);
$eventDispatcher->dispatch($event, PageEditEvent::FORM_INIT_EVENT);
$this->getConfiguration()->setFormOptions('edit', [
'page_configuration' => $pageLocator->getPage(get_class($entity)),
'page_builder_options' => $event->getPageBuilderOptions(),
]);
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/site/page/delete/{entity}", name="admin_site_page_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
}
/**
* @Route("/admin/site/page/batch/{page}", name="admin_site_page_batch", methods={"POST"}, requirements={"page":"\d+"})
*/
public function batch(RepositoryQuery $query, EntityManager $entityManager, Request $request, Session $session, int $page = 1): Response
{
return $this->doBatch($page, $query, $entityManager, $request, $session);
}
protected function getConfiguration(): CrudConfiguration
{
return CrudConfiguration::create()
->setPageTitle('index', 'Pages')
->setPageTitle('edit', '{name}')
->setPageTitle('show', '{name}')
->setPageRoute('index', 'admin_site_page_index')
->setPageRoute('edit', 'admin_site_page_edit')
->setPageRoute('delete', 'admin_site_page_delete')
->setPageRoute('filter', 'admin_site_page_filter')
->setPageRoute('batch', 'admin_site_page_batch')
->setForm('edit', Type::class, [])
->setForm('filter', FilterType::class)
->setView('form', '@Core/site/page_admin/_form.html.twig')
->setAction('index', 'new', false)
->setAction('index', 'show', false)
->setAction('edit', 'show', false)
->setField('index', 'Name', Field\TextField::class, [
'property' => 'name',
'sort' => ['name', '.name'],
'attr' => ['class' => 'col-4'],
])
->setField('index', 'Elements', Field\TextField::class, [
'view' => '@Core/site/page_admin/fields/nodes.html.twig',
'sort' => ['navigation', function (RepositoryQuery $query, $direction) {
$query
->leftJoin('.nodes', 'node')
->leftJoin('node.menu', 'menu')
->leftJoin('menu.navigation', 'navigation')
->orderBy('navigation.label', $direction)
;
}],
'attr' => ['class' => 'col-6'],
])
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity);
})
;
}
protected function getSection(): string
{
return 'site_page';
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Core\Controller\Site;
use App\Core\Site\SiteRequest;
use App\Core\Site\SiteStore;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class PageController extends AbstractController
{
protected SiteRequest $siteRequest;
protected SiteStore $siteStore;
public function __construct(SiteRequest $siteRequest, SiteStore $siteStore)
{
$this->siteRequest = $siteRequest;
$this->siteStore = $siteStore;
}
public function show(): Response
{
if (!$this->siteRequest->getPage()) {
throw $this->createNotFoundException();
}
return $this->defaultRender($this->siteRequest->getPage()->getTemplate());
}
protected function defaultRender(string $view, array $parameters = [], Response $response = null): Response
{
$parameters = array_merge($this->getDefaultRenderParameters(), $parameters);
if (null === $response) {
$contentType = $this->siteRequest->getNode()->getContentType();
$response = new Response(null, 200, [
'Content-Type' => $contentType ?? 'text/html',
]);
}
return parent::render($view, $parameters, $response);
}
protected function getDefaultRenderParameters(): array
{
return [
'_node' => $this->siteRequest->getNode(),
'_page' => $this->siteRequest->getPage(),
'_menu' => $this->siteRequest->getMenu(),
'_navigation' => $this->siteRequest->getNavigation(),
'_domain' => $this->siteRequest->getDomain(),
'_locale' => $this->siteRequest->getNavigation()->getLocale(),
'_store' => $this->siteStore,
];
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Core\Controller\Site;
use App\Core\Repository\Site\NavigationRepositoryQuery;
use App\Core\Sitemap\SitemapBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SitemapController extends AbstractController
{
/**
* @Route("/sitemap.xml", name="sitemap")
*/
public function sitemap(Request $request, NavigationRepositoryQuery $navigationRepositoryQuery, SitemapBuilder $builder): Response
{
$navigations = $navigationRepositoryQuery
->whereDomain($request->getHost())
->find()
;
$items = [];
foreach ($navigations as $navigation) {
$items = array_merge(
$items,
$builder->build($navigation)
);
}
$response = new Response();
$response->headers->set('Content-Type', 'text/xml');
return $this->render('@Core/site/sitemap/sitemap.xml.twig', [
'items' => $items,
], $response);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Core\Controller\Site;
use App\Core\Controller\Admin\AdminController;
use App\Core\Entity\Site\Navigation;
use App\Core\Factory\Site\MenuFactory;
use App\Core\Form\Site\MenuType;
use App\Core\Repository\Site\NavigationRepositoryQuery;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/site/tree")
*/
class TreeAdminController extends AdminController
{
/**
* @Route("/", name="admin_site_tree_index")
*/
public function index(NavigationRepositoryQuery $navigationQuery, Session $session): Response
{
$navigation = null;
if ($session->has('site_tree_last_navigation')) {
$navigation = $navigationQuery->create()
->filterById((int) $session->get('site_tree_last_navigation'))
->findOne()
;
}
if (null === $navigation) {
$navigation = $navigationQuery->create()
->orderBy('.sortOrder')
->findOne()
;
}
if (null === $navigation) {
$this->addFlash('warning', 'You must add a navigation.');
return $this->redirectToRoute('admin_site_navigation_new');
}
return $this->redirectToRoute('admin_site_tree_navigation', [
'navigation' => $navigation->getId(),
]);
}
/**
* @Route("/navigation/{navigation}", name="admin_site_tree_navigation")
*/
public function navigation(
Navigation $navigation,
NavigationRepositoryQuery $navigationQuery,
MenuFactory $menuFactory,
Session $session
): Response {
$navigations = $navigationQuery->create()
->orderBy('.sortOrder')
->find()
;
$session->set('site_tree_last_navigation', $navigation->getId());
$forms = [
'menu' => $this->createForm(MenuType::class, $menuFactory->create())->createView(),
'menus' => [],
];
foreach ($navigation->getMenus() as $menu) {
$forms['menus'][$menu->getId()] = $this->createForm(MenuType::class, $menu)->createView();
}
return $this->render('@Core/site/tree_admin/navigation.html.twig', [
'navigation' => $navigation,
'navigations' => $navigations,
'forms' => $forms,
]);
}
public function getSection(): string
{
return 'site_tree';
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Core\Controller\Task;
use App\Core\Controller\Admin\AdminController;
use App\Core\Event\Task\TaskInitEvent;
use App\Core\Event\Task\TaskRunRequestedEvent;
use SensioLabs\AnsiConverter\AnsiToHtmlConverter;
use SensioLabs\AnsiConverter\Theme\SolarizedTheme;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/admin/task")
*/
class TaskAdminController extends AdminController
{
/**
* @Route("/", name="admin_task_index")
*/
public function index(EventDispatcherInterface $eventDispatcher): Response
{
$event = new TaskInitEvent();
$eventDispatcher->dispatch($event, TaskInitEvent::INIT_EVENT);
return $this->render('@Core/task/task_admin/index.html.twig', [
'pager' => $event->getTasks(),
]);
}
/**
* @Route("/run/{task}", name="admin_task_run", methods={"GET"})
*/
public function run(
string $task,
Request $request,
EventDispatcherInterface $eventDispatcher
): Response {
if (!$this->isCsrfTokenValid('task_run', $request->query->get('_token'))) {
throw $this->createAccessDeniedException();
}
$output = new BufferedOutput();
$event = new TaskRunRequestedEvent($task, $request->query, $output);
$eventDispatcher->dispatch($event, TaskRunRequestedEvent::RUN_REQUEST_EVENT);
$converter = new AnsiToHtmlConverter(new SolarizedTheme());
$content = $converter->convert($output->fetch());
return $this->render('@Core/task/task_admin/run.html.twig', [
'output' => $content,
]);
}
public function getSection(): string
{
return 'task';
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace App\Core\Controller\User;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field;
use App\Core\Event\Account\PasswordRequestEvent;
use App\Core\Factory\UserFactory as Factory;
use App\Core\Manager\EntityManager;
use App\Entity\User as Entity;
use App\Form\UserType as Type;
use App\Repository\UserRepositoryQuery as RepositoryQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use App\Core\Security\TokenGenerator;
class UserAdminController extends CrudController
{
/**
* @Route("/admin/user/{page}", name="admin_user_index", methods={"GET"}, requirements={"page":"\d+"})
*/
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return $this->doIndex($page, $query, $request, $session);
}
/**
* @Route("/admin/user/new", name="admin_user_new", methods={"GET", "POST"})
*/
public function new(Factory $factory, EntityManager $entityManager, Request $request, TokenGenerator $tokenGenerator): Response
{
return $this->doNew($factory->create(null, $tokenGenerator->generateToken()), $entityManager, $request);
}
/**
* @Route("/admin/user/show/{entity}", name="admin_user_show", methods={"GET"})
*/
public function show(Entity $entity): Response
{
return $this->doShow($entity);
}
/**
* @Route("/admin/user/filter", name="admin_user_filter", methods={"GET"})
*/
public function filter(Session $session): Response
{
return $this->doFilter($session);
}
/**
* @Route("/admin/user/edit/{entity}", name="admin_user_edit", methods={"GET", "POST"})
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doEdit($entity, $entityManager, $request);
}
/**
* @Route("/admin/user/delete/{entity}", name="admin_user_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doDelete($entity, $entityManager, $request);
}
/**
* @Route("/admin/user/resetting_request/{entity}", name="admin_user_resetting_request", methods={"POST"})
*/
public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response
{
if ($this->isCsrfTokenValid('resetting_request'.$entity->getId(), $request->request->get('_token'))) {
$eventDispatcher->dispatch(new PasswordRequestEvent($entity), PasswordRequestEvent::EVENT);
$this->addFlash('success', 'E-mail sent.');
}
return $this->redirectToRoute('admin_user_edit', [
'entity' => $entity->getId(),
]);
}
protected function getConfiguration(): CrudConfiguration
{
return CrudConfiguration::create()
->setPageTitle('index', 'Users')
->setPageTitle('edit', '{username}')
->setPageTitle('new', 'New user')
->setPageTitle('show', '{username}')
->setPageRoute('index', 'admin_user_index')
->setPageRoute('new', 'admin_user_new')
->setPageRoute('edit', 'admin_user_edit')
->setPageRoute('show', 'admin_user_show')
->setPageRoute('delete', 'admin_user_delete')
->setPageRoute('filter', 'admin_user_filter')
->setForm('edit', Type::class, [])
->setForm('new', Type::class)
->setView('form', '@Core/user/user_admin/_form.html.twig')
->setView('index', '@Core/user/user_admin/index.html.twig')
->setView('new', '@Core/user/user_admin/new.html.twig')
->setView('show', '@Core/user/user_admin/show.html.twig')
->setView('show_entity', '@Core/user/user_admin/_show.html.twig')
->setView('edit', '@Core/user/user_admin/edit.html.twig')
->setDefaultSort('index', 'username')
->setField('index', 'E-mail', Field\TextField::class, [
'property' => 'email',
'sort' => ['email', '.email'],
'attr' => ['class' => 'miw-200'],
])
->setField('index', 'Display name', Field\TextField::class, [
'property' => 'displayName',
'sort' => ['displayName', '.displayName'],
'attr' => ['class' => 'miw-200'],
])
;
}
protected function getSection(): string
{
return 'user';
}
}

View File

@ -0,0 +1,330 @@
<?php
namespace App\Core\Crud;
use App\Core\Crud\Exception\CrudConfigurationException;
/**
* class CrudConfiguration.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CrudConfiguration
{
protected array $pageTitles = [];
protected array $pageRoutes = [];
protected array $pageRouteParams = [];
protected array $actions = [];
protected array $batchActions = [];
protected array $actionTitles = [];
protected array $forms = [];
protected array $formOptions = [];
protected array $views = [];
protected array $viewDatas = [];
protected array $fields = [];
protected array $maxPerPage = [];
protected array $locales = [];
protected array $defaultSort = [];
protected array $isSortableCollection = [];
protected string $sortableCollectionProperty = 'sortOrder';
protected ?string $defaultLocale = null;
protected bool $showActions = true;
protected static $self;
public static function create()
{
if (null === self::$self) {
self::$self = new self();
}
return self::$self;
}
/* -- */
public function setPageTitle(string $page, string $title): self
{
$this->pageTitles[$page] = $title;
return $this;
}
public function getPageTitle(string $page, ?string $default = null): ?string
{
return $this->pageTitles[$page] ?? $default;
}
/* -- */
public function setPageRoute(string $page, string $route): self
{
$this->pageRoutes[$page] = $route;
return $this;
}
public function getPageRoute(string $page): ?string
{
return $this->pageRoutes[$page];
}
public function setPageRouteParams(string $page, array $params): self
{
$this->pageRouteParams[$page] = $params;
return $this;
}
public function getPageRouteParams(string $page): array
{
return $this->pageRouteParams[$page] ?? [];
}
/* -- */
public function setForm(string $context, string $form, array $options = []): self
{
$this->forms[$context] = $form;
return $this;
}
public function getForm(string $context): ?string
{
return $this->forms[$context] ?? null;
}
public function setFormOptions(string $context, array $options = []): self
{
$this->formOptions[$context] = $options;
return $this;
}
public function getFormOptions(string $context): array
{
return $this->formOptions[$context] ?? [];
}
/* -- */
public function setAction(string $page, string $action, bool $enabled): self
{
if (!isset($this->actions[$page])) {
$this->actions[$page] = [];
}
$this->actions[$page][$action] = $enabled;
return $this;
}
public function getAction(string $page, string $action, bool $default = true)
{
return $this->actions[$page][$action] ?? $default;
}
public function setBatchAction(string $page, string $action, string $label, callable $callback): self
{
if (!isset($this->batchActions[$page])) {
$this->batchActions[$page] = [];
}
$this->batchActions[$page][$action] = [
'label' => $label,
'callback' => $callback,
];
return $this;
}
public function getBatchActions(string $page)
{
return $this->batchActions[$page] ?? [];
}
public function getBatchAction(string $page, string $action)
{
return $this->batchActions[$page][$action] ?? null;
}
public function hasBatchAction(string $page)
{
return !empty($this->batchActions[$page]);
}
/* -- */
public function setActionTitle(string $page, string $action, string $title): self
{
if (!isset($this->actionTitles[$page])) {
$this->actionTitles[$page] = [];
}
$this->actions[$page][$action] = $title;
return $this;
}
public function getActionTitle(string $page, string $action, ?string $default = null): ?string
{
return $this->actionTitles[$page][$action] ?? $default;
}
/* -- */
public function setView(string $context, string $view): self
{
$this->views[$context] = $view;
return $this;
}
public function getView(string $context, ?string $default = null)
{
if (null === $default) {
$default = sprintf('@Core/admin/crud/%s.html.twig', $context);
}
return $this->views[$context] ?? $default;
}
public function addViewData(string $context, string $name, $value): self
{
if (!isset($this->viewDatas[$context])) {
$this->viewDatas[$context] = [];
}
$this->viewDatas[$context][$name] = $value;
return $this;
}
public function setViewDatas(string $context, array $datas): self
{
foreach ($datas as $name => $value) {
$this->addViewData($name, $value);
}
return $this;
}
public function getViewDatas(string $context): array
{
return $this->viewDatas[$context] ?? [];
}
/* -- */
public function setField(string $context, string $label, string $field, array $options): self
{
if (!isset($this->fields[$context])) {
$this->fields[$context] = [];
}
$this->fields[$context][$label] = [
'field' => $field,
'options' => $options,
];
return $this;
}
public function getFields(string $context): array
{
return $this->fields[$context] ?? [];
}
/* -- */
public function setMaxPerPage(string $page, int $max)
{
$this->maxPerPage[$page] = $max;
return $this;
}
public function getMaxPerPage(string $page, int $default = 20)
{
return $this->maxPerPage[$page] ?? $default;
}
/* -- */
public function setI18n(array $locales, string $defaultLocale): self
{
$this->locales = $locales;
$this->defaultLocale = $defaultLocale;
return $this;
}
public function getLocales(): array
{
return $this->locales;
}
public function getDefaultLocale(): ?string
{
return $this->defaultLocale;
}
public function isI18n(): bool
{
return !empty($this->locales);
}
/* -- */
public function setDefaultSort(string $context, string $label, string $direction = 'asc'): self
{
$this->defaultSort[$context] = [
'label' => $label,
'direction' => $direction,
];
return $this;
}
public function getDefaultSort(string $context)
{
return $this->defaultSort[$context] ?? null;
}
public function setIsSortableCollection(string $page, bool $isSortableCollection): self
{
$this->isSortableCollection[$page] = $isSortableCollection;
return $this;
}
public function getIsSortableCollection(string $page): bool
{
return $this->isSortableCollection[$page] ?? false;
}
public function setSortableCollectionProperty(string $sortableCollectionProperty): self
{
$this->sortableCollectionProperty = $sortableCollectionProperty;
return $this;
}
public function getSortableCollectionProperty(): string
{
return $this->sortableCollectionProperty;
}
public function setShowActions(bool $showActions): self
{
$this->showActions = $showActions;
return $this;
}
public function getShowActions(): bool
{
return $this->showActions;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Core\Crud\Exception;
/**
* class CrudConfigurationException.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CrudConfigurationException extends \Exception
{
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Twig\Environment;
/**
* class ButtonField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class ButtonField extends Field
{
public function buildView(Environment $twig, $entity, array $options, ?string $locale = null)
{
if (isset($options['button_attr_builder']) && is_callable($options['button_attr_builder'])) {
$options['button_attr'] = (array) call_user_func($options['button_attr_builder'], $entity, $options);
}
return parent::buildView($twig, $entity, $options, $locale);
}
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/button.html.twig',
'button_attr' => [],
'button_attr_builder' => null,
'button_tag' => 'button',
]);
$resolver->setAllowedTypes('button_attr', ['array']);
$resolver->setAllowedTypes('button_tag', ['string']);
$resolver->setAllowedTypes('button_attr_builder', ['null', 'callable']);
return $resolver;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class DateField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class DateField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/date.html.twig',
'format' => 'Y-m-d',
]);
return $resolver;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class DatetimeField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class DatetimeField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/date.html.twig',
'format' => 'Y-m-d H:i:s',
]);
return $resolver;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Twig\Environment;
/**
* class Field.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class Field
{
public function buildView(Environment $twig, $entity, array $options, ?string $locale = null)
{
return $twig->render($this->getView($options), [
'entity' => $entity,
'value' => $this->getValue($entity, $options, $locale),
'options' => $options,
]);
}
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
$resolver->setDefaults([
'property' => null,
'property_builder' => null,
'view' => null,
'raw' => false,
'sort' => null,
'href' => null,
'href_attr' => [],
'attr' => [],
]);
$resolver->setRequired('view');
$resolver->setAllowedTypes('property', ['null', 'string']);
$resolver->setAllowedTypes('view', 'string');
$resolver->setAllowedTypes('attr', 'array');
$resolver->setAllowedTypes('href', ['null', 'string', 'callable']);
$resolver->setAllowedTypes('href_attr', 'array', 'callable');
$resolver->setAllowedTypes('raw', 'boolean');
$resolver->setAllowedTypes('property_builder', ['null', 'callable']);
$resolver->setAllowedValues('sort', function($value) {
if ($value === null) {
return true;
}
if (!is_array($value)) {
return false;
}
$isValidParam1 = !empty($value[0]) && is_string($value[0]);
$isValidParam2 = !empty($value[1]) && (is_string($value[1]) || is_callable($value[1]));
return $isValidParam1 && $isValidParam2;
});
return $resolver;
}
protected function getValue($entity, array $options, ?string $locale = null)
{
if (null !== $options['property']) {
$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()->getPropertyAccessor();
try {
$value = $propertyAccessor->getValue($entity, $options['property']);
} catch (NoSuchPropertyException $e) {
if (null !== $locale) {
$value = $propertyAccessor->getValue($entity->translate($locale), $options['property']);
} else {
throw $e;
}
}
} elseif (null !== $options['property_builder']) {
$value = call_user_func($options['property_builder'], $entity, $options);
} else {
$value = null;
}
return $value;
}
protected function getView(array $options)
{
return $options['view'];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class ImageField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class ImageField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/image.html.twig',
'image_attr' => [],
]);
$resolver->setAllowedTypes('image_attr', ['array']);
return $resolver;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Core\Crud\Field;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* class TextField.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class TextField extends Field
{
public function configureOptions(OptionsResolver $resolver): OptionsResolver
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'view' => '@Core/admin/crud/field/text.html.twig',
]);
return $resolver;
}
}

View File

@ -0,0 +1,138 @@
<?php
namespace App\Core\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$defaultMimetypes = [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'video/mp4',
'audio/mpeg3',
'audio/x-mpeg-3',
'multipart/x-zip',
'multipart/x-gzip',
'application/pdf',
'application/ogg',
'application/zip',
'application/rar',
'application/x-rar-compressed',
'application/x-zip-compressed',
'application/tar',
'application/x-tar',
'application/x-bzip',
'application/x-bzip2',
'application/x-gzip',
'application/octet-stream',
'application/msword',
'text/plain',
'text/css',
];
$defaultLocked = [
'%kernel.project_dir%/public/uploads',
];
$treeBuilder = new TreeBuilder('core');
$treeBuilder->getRootNode()
->children()
->arrayNode('site')
->children()
->scalarNode('name')
->defaultValue('Murph')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('logo')
->defaultValue('build/images/core/logo.svg')
->isRequired()
->cannotBeEmpty()
->end()
->arrayNode('controllers')
->prototype('array')
->children()
->scalarNode('name')
->cannotBeEmpty()
->end()
->scalarNode('action')
->cannotBeEmpty()
->end()
->end()
->end()
->end()
->arrayNode('security')
->children()
->arrayNode('roles')
->prototype('array')
->children()
->scalarNode('name')
->cannotBeEmpty()
->end()
->scalarNode('role')
->cannotBeEmpty()
->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('pages')
->prototype('array')
->children()
->scalarNode('name')
->isRequired()
->cannotBeEmpty()
->end()
->arrayNode('templates')
->prototype('array')
->children()
->scalarNode('name')
->cannotBeEmpty()
->end()
->scalarNode('file')
->cannotBeEmpty()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('file_manager')
->children()
->arrayNode('mimes')
->scalarPrototype()
->end()
->defaultValue($defaultMimetypes)
->end()
->scalarNode('path')
->defaultValue('%kernel.project_dir%/public/uploads')
->cannotBeEmpty()
->end()
->scalarNode('path_uri')
->defaultValue('/uploads')
->cannotBeEmpty()
->end()
->arrayNode('path_locked')
->scalarPrototype()
->end()
->defaultValue($defaultLocked)
->end()
->end()
->end()
->end();
return $treeBuilder;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Core\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
class CoreExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('core', $config);
}
/**
* {@inheritdoc}
*/
public function getConfiguration(array $configs, ContainerBuilder $container)
{
return new Configuration();
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Core\Doctrine;
use Doctrine\ORM\Mapping as ORM;
trait Timestampable
{
/**
* @ORM\Column(name="created_at", type="datetime")
*/
protected $createdAt;
/**
* @ORM\Column(name="updated_at", type="datetime")
*/
protected $updatedAt;
/**
* @ORM\PrePersist
*/
public function onPrePersist(): void
{
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}
/**
* @ORM\PreUpdate
*/
public function onPreUpdate(): void
{
$this->updatedAt = new \DateTime();
}
public function setCreatedAt(?\DateTime $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getCreatedAt(): ?\DateTime
{
return $this->createdAt;
}
public function setUpdatedAt(?\DateTime $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getUpdatedAt(): ?\DateTime
{
return $this->updatedAt;
}
}

0
src/core/Entity/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,103 @@
<?php
namespace App\Core\Entity\Analytic;
use App\Core\Entity\Site\Node;
use App\Repository\Entity\Analytic\NodeViewRepository;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
/**
* @ORM\Entity(repositoryClass=ViewRepository::class)
* @ORM\Table(name="analytic_referer")
*/
class Referer implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="analyticReferers")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
protected $node;
/**
* @ORM\Column(type="string", length=255)
*/
protected $uri;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
protected $views = 0;
/**
* @ORM\Column(type="date")
*/
protected $date;
public function getId(): ?int
{
return $this->id;
}
public function getNode(): ?Node
{
return $this->node;
}
public function setNode(?Node $node): self
{
$this->node = $node;
return $this;
}
public function getUri(): ?string
{
return $this->uri;
}
public function setUri(string $uri): self
{
$this->uri = $uri;
return $this;
}
public function getViews(): ?int
{
return $this->views;
}
public function setViews(int $views): self
{
$this->views = $views;
return $this;
}
public function addView(): self
{
++$this->views;
return $this;
}
public function getDate(): ?\DateTimeInterface
{
return $this->date;
}
public function setDate(\DateTimeInterface $date): self
{
$this->date = $date;
return $this;
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace App\Core\Entity\Analytic;
use App\Core\Entity\Site\Node;
use App\Repository\Entity\Analytic\NodeViewRepository;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
/**
* @ORM\Entity(repositoryClass=ViewRepository::class)
* @ORM\Table(name="analytic_view")
*/
class View implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="analyticViews")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
protected $node;
/**
* @ORM\Column(type="string", length=255)
*/
protected $path;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
protected $views = 0;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
protected $desktopViews = 0;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
protected $mobileViews = 0;
/**
* @ORM\Column(type="date")
*/
protected $date;
public function getId(): ?int
{
return $this->id;
}
public function getNode(): ?Node
{
return $this->node;
}
public function setNode(?Node $node): self
{
$this->node = $node;
return $this;
}
public function getPath(): ?string
{
return $this->path;
}
public function setPath(string $path): self
{
$this->path = $path;
return $this;
}
public function getViews(): ?int
{
return $this->views;
}
public function setViews(int $views): self
{
$this->views = $views;
return $this;
}
public function addView(): self
{
++$this->views;
return $this;
}
public function getDesktopViews(): ?int
{
return $this->desktopViews;
}
public function setDesktopViews(int $desktopViews): self
{
$this->desktopViews = $desktopViews;
return $this;
}
public function addDesktopView(): self
{
++$this->desktopViews;
return $this;
}
public function getMobileViews(): ?int
{
return $this->mobileViews;
}
public function setMobileViews(int $mobileViews): self
{
$this->mobileViews = $mobileViews;
return $this;
}
public function addMobileView(): self
{
++$this->mobileViews;
return $this;
}
public function getDate(): ?\DateTimeInterface
{
return $this->date;
}
public function setDate(\DateTimeInterface $date): self
{
$this->date = $date;
return $this;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Core\Entity;
interface EntityInterface
{
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Core\Entity;
use App\Repository\Entity\FileInformationRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=FileInformationRepository::class)
*/
class FileInformation implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="NONE")
* @ORM\Column(type="string", length=96, unique=true)
*/
protected $id;
/**
* @ORM\Column(type="text", nullable=true)
*/
protected $attributes;
public function getId(): ?string
{
return $this->id;
}
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
public function getAttributes()
{
return (array) json_decode($this->attributes, true);
}
public function setAttributes($attributes): self
{
$this->attributes = json_encode($attributes);
return $this;
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace App\Core\Entity;
use App\Core\Entity\Site\Navigation;
use App\Core\Repository\NavigationSettingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=NavigationSettingRepository::class)
*/
class NavigationSetting implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $section;
/**
* @ORM\Column(type="string", length=255)
*/
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
protected $code;
/**
* @ORM\Column(type="text", nullable=true)
*/
protected $value;
/**
* @ORM\ManyToOne(targetEntity=Navigation::class, inversedBy="navigationSettings")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
protected $navigation;
public function getId(): ?int
{
return $this->id;
}
public function getSection(): ?string
{
return $this->section;
}
public function setSection(string $section): self
{
$this->section = $section;
return $this;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getValue()
{
return json_decode($this->value, true);
}
public function setValue($value): self
{
$this->value = json_encode($value);
return $this;
}
public function getNavigation(): ?Navigation
{
return $this->navigation;
}
public function setNavigation(?Navigation $navigation): self
{
$this->navigation = $navigation;
return $this;
}
}

View File

@ -0,0 +1,211 @@
<?php
namespace App\Core\Entity;
use App\Core\Repository\RedirectRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=RedirectRepository::class)
*/
class Redirect implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=5)
*/
protected $scheme;
/**
* @ORM\Column(type="string", length=255)
*/
protected $domain;
/**
* @ORM\Column(type="string", length=6)
*/
protected $domainType;
/**
* @ORM\Column(type="string", length=255)
*/
protected $rule;
/**
* @ORM\Column(type="string", length=6)
*/
protected $ruleType;
/**
* @ORM\Column(type="string", length=255)
*/
protected $location;
/**
* @ORM\Column(type="integer")
*/
protected $redirectCode;
/**
* @ORM\Column(type="string", length=255)
*/
protected $label;
/**
* @ORM\Column(type="integer", nullable=true)
*/
protected $sortOrder;
/**
* @ORM\Column(type="boolean")
*/
protected $isEnabled;
/**
* @ORM\Column(type="boolean")
*/
protected $reuseQueryString;
public function getId(): ?int
{
return $this->id;
}
public function getScheme(): ?string
{
return $this->scheme;
}
public function setScheme(string $scheme): self
{
$this->scheme = $scheme;
return $this;
}
public function getDomain(): ?string
{
return $this->domain;
}
public function setDomain(string $domain): self
{
$this->domain = $domain;
return $this;
}
public function getDomainType(): ?string
{
return $this->domainType;
}
public function setDomainType(string $domainType): self
{
$this->domainType = $domainType;
return $this;
}
public function getRule(): ?string
{
return $this->rule;
}
public function setRule(string $rule): self
{
$this->rule = $rule;
return $this;
}
public function getRuleType(): ?string
{
return $this->ruleType;
}
public function setRuleType(string $ruleType): self
{
$this->ruleType = $ruleType;
return $this;
}
public function getLocation(): ?string
{
return $this->location;
}
public function setLocation(string $location): self
{
$this->location = $location;
return $this;
}
public function getRedirectCode(): ?int
{
return $this->redirectCode;
}
public function setRedirectCode(int $redirectCode): self
{
$this->redirectCode = $redirectCode;
return $this;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getSortOrder(): ?int
{
return $this->sortOrder;
}
public function setSortOrder(?int $sortOrder): self
{
$this->sortOrder = $sortOrder;
return $this;
}
public function getIsEnabled(): ?bool
{
return $this->isEnabled;
}
public function setIsEnabled(bool $isEnabled): self
{
$this->isEnabled = $isEnabled;
return $this;
}
public function getReuseQueryString(): ?bool
{
return $this->reuseQueryString;
}
public function setReuseQueryString(bool $reuseQueryString): self
{
$this->reuseQueryString = $reuseQueryString;
return $this;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Core\Entity;
use App\Core\Repository\SettingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=SettingRepository::class)
*/
class Setting implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $section;
/**
* @ORM\Column(type="string", length=255)
*/
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
protected $code;
/**
* @ORM\Column(type="text", nullable=true)
*/
protected $value;
public function getId(): ?int
{
return $this->id;
}
public function getSection(): ?string
{
return $this->section;
}
public function setSection(string $section): self
{
$this->section = $section;
return $this;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getValue()
{
return json_decode($this->value, true);
}
public function setValue($value): self
{
$this->value = json_encode($value);
return $this;
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace App\Core\Entity\Site;
use App\Core\Doctrine\Timestampable;
use App\Core\Entity\EntityInterface;
use App\Core\Repository\Site\MenuRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=MenuRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class Menu implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
protected $code;
/**
* @ORM\ManyToOne(targetEntity=Navigation::class, inversedBy="menus")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
protected $navigation;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="menu", orphanRemoval=true, cascade={"remove", "persist"})
*/
protected $nodes;
/**
* @ORM\OneToOne(targetEntity=Node::class, cascade={"persist"})
* @ORM\JoinColumn(onDelete="CASCADE")
*/
protected $rootNode;
public function __construct()
{
$this->nodes = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getNavigation(): ?Navigation
{
return $this->navigation;
}
public function setNavigation(?Navigation $navigation): self
{
$this->navigation = $navigation;
return $this;
}
/**
* @return Collection|Node[]
*/
public function getNodes(): Collection
{
return $this->nodes;
}
public function addNode(Node $node): self
{
if (!$this->nodes->contains($node)) {
$this->nodes[] = $node;
$node->setMenu($this);
}
return $this;
}
public function removeNode(Node $node): self
{
if ($this->nodes->removeElement($node)) {
// set the owning side to null (unless already changed)
if ($node->getMenu() === $this) {
$node->setMenu(null);
}
}
return $this;
}
public function getRootNode(): ?Node
{
return $this->rootNode;
}
public function setRootNode(?Node $rootNode): self
{
$this->rootNode = $rootNode;
return $this;
}
public function getRouteName(): string
{
return $this->getNavigation()->getRouteName().'_'.($this->getCode() ? $this->getCode() : $this->getId());
}
}

View File

@ -0,0 +1,243 @@
<?php
namespace App\Core\Entity\Site;
use App\Core\Doctrine\Timestampable;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\NavigationSetting;
use App\Core\Repository\Site\NavigationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=NavigationRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class Navigation implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $label;
/**
* @ORM\Column(type="string", length=255)
*/
protected $code;
/**
* @ORM\Column(type="string", length=255)
*/
protected $domain;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
protected $forceDomain = false;
/**
* @ORM\Column(type="text", nullable=true)
*/
protected $additionalDomains = '[]';
/**
* @ORM\OneToMany(targetEntity=Menu::class, mappedBy="navigation")
*/
protected $menus;
/**
* @ORM\Column(type="string", length=10)
*/
protected $locale = 'en';
/**
* @ORM\Column(type="integer", nullable=true)
*/
protected $sortOrder;
/**
* @ORM\OneToMany(targetEntity=NavigationSetting::class, mappedBy="navigation", orphanRemoval=true)
*/
protected $navigationSettings;
public function __construct()
{
$this->menus = new ArrayCollection();
$this->navigationSettings = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): self
{
$this->code = $code;
return $this;
}
public function getDomain(): ?string
{
return $this->domain;
}
public function setDomain(string $domain): self
{
$this->domain = $domain;
return $this;
}
public function getForceDomain(): ?bool
{
return $this->forceDomain;
}
public function setForceDomain(bool $forceDomain): self
{
$this->forceDomain = $forceDomain;
return $this;
}
public function getAdditionalDomains(): array
{
return (array) json_decode($this->additionalDomains, true);
}
public function setAdditionalDomains(array $additionalDomains): self
{
$this->additionalDomains = json_encode($additionalDomains);
return $this;
}
/**
* @return Collection|Menu[]
*/
public function getMenus(): Collection
{
return $this->menus;
}
public function addMenu(Menu $menu): self
{
if (!$this->menus->contains($menu)) {
$this->menus[] = $menu;
$menu->setNavigation($this);
}
return $this;
}
public function removeMenu(Menu $menu): self
{
if ($this->menus->removeElement($menu)) {
// set the owning side to null (unless already changed)
if ($menu->getNavigation() === $this) {
$menu->setNavigation(null);
}
}
return $this;
}
public function getMenu(string $code): ?Menu
{
foreach ($this->menus as $menu) {
if ($menu->getCode() === $code) {
return $menu;
}
}
return $menu;
}
public function getRouteName(): string
{
return $this->getCode() ? $this->getCode() : 'navigation_'.$this->getId();
}
public function getLocale(): ?string
{
return $this->locale;
}
public function setLocale(string $locale): self
{
$this->locale = $locale;
return $this;
}
public function getSortOrder(): ?int
{
return $this->sortOrder;
}
public function setSortOrder(?int $sortOrder): self
{
$this->sortOrder = $sortOrder;
return $this;
}
/**
* @return Collection|NavigationSetting[]
*/
public function getNavigationSettings(): Collection
{
return $this->navigationSettings;
}
public function addNavigationSetting(NavigationSetting $navigationSetting): self
{
if (!$this->navigationSettings->contains($navigationSetting)) {
$this->navigationSettings[] = $navigationSetting;
$navigationSetting->setNavigation($this);
}
return $this;
}
public function removeNavigationSetting(NavigationSetting $navigationSetting): self
{
if ($this->navigationSettings->removeElement($navigationSetting)) {
// set the owning side to null (unless already changed)
if ($navigationSetting->getNavigation() === $this) {
$navigationSetting->setNavigation(null);
}
}
return $this;
}
}

View File

@ -0,0 +1,676 @@
<?php
namespace App\Core\Entity\Site;
use App\Core\Doctrine\Timestampable;
use App\Core\Entity\Analytic\Referer;
use App\Core\Entity\Analytic\View;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Page\Page;
use App\Core\Repository\Site\NodeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use function Symfony\Component\String\u;
/**
* @Gedmo\Tree(type="nested")
* @ORM\HasLifecycleCallbacks
* @ORM\Entity(repositoryClass=NodeRepository::class)
*/
class Node implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity=Menu::class, inversedBy="nodes", cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
protected $menu;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $label;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $url;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
protected $disableUrl = false;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
protected $isVisible = false;
/**
* @Gedmo\TreeLeft
* @ORM\Column(type="integer")
*/
protected $treeLeft;
/**
* @Gedmo\TreeLevel
* @ORM\Column(type="integer")
*/
protected $treeLevel;
/**
* @Gedmo\TreeRight
* @ORM\Column(type="integer")
*/
protected $treeRight;
/**
* @Gedmo\TreeRoot
* @ORM\ManyToOne(targetEntity="Node")
* @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
*/
protected $treeRoot;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Node", inversedBy="children")
* @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
*/
protected $parent;
/**
* @ORM\OneToMany(targetEntity="Node", mappedBy="parent")
* @ORM\OrderBy({"treeLeft"="ASC"})
*/
protected $children;
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="nodes", cascade={"persist"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
protected $page;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $code;
/**
* @ORM\Column(type="array", nullable=true)
*/
protected $parameters = [];
/**
* @ORM\Column(type="array", nullable=true)
*/
protected $attributes = [];
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $controller;
/**
* @ORM\Column(type="array", nullable=true)
*/
protected $sitemapParameters = [];
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="aliasNodes")
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
protected $aliasNode;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="aliasNode")
*/
protected $aliasNodes;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $contentType;
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
protected $enableAnalytics = false;
/**
* @ORM\OneToMany(targetEntity=View::class, mappedBy="node")
*/
protected $analyticViews;
/**
* @ORM\OneToMany(targetEntity=Referer::class, mappedBy="node")
*/
protected $analyticReferers;
/**
* @ORM\Column(type="array")
*/
private $securityRoles = [];
/**
* @ORM\Column(type="string", length=3, options={"default"="or"})
*/
private $securityOperator = 'or';
public function __construct()
{
$this->children = new ArrayCollection();
$this->aliasNodes = new ArrayCollection();
$this->analyticViews = new ArrayCollection();
$this->analyticReferers = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getMenu(): ?Menu
{
return $this->menu;
}
public function setMenu(?Menu $menu): self
{
$this->menu = $menu;
return $this;
}
public function getTreeLeft(): ?int
{
return $this->treeLeft;
}
public function setTreeLeft(int $treeLeft): self
{
$this->treeLeft = $treeLeft;
return $this;
}
public function getTreeLevel(): ?int
{
return $this->treeLevel;
}
public function setTreeLevel(int $treeLevel): self
{
$this->treeLevel = $treeLevel;
return $this;
}
public function getTreeRight(): ?int
{
return $this->treeRight;
}
public function setTreeRight(int $treeRight): self
{
$this->treeRight = $treeRight;
return $this;
}
public function getTreeRoot(): ?self
{
return $this->treeRoot;
}
public function setTreeRoot(?self $treeRoot): self
{
$this->treeRoot = $treeRoot;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
return $this;
}
/**
* @return Collection|Node[]
*/
public function getChildren(array $criteria = []): Collection
{
if (null === $this->children) {
$this->children = new ArrayCollection();
}
if (!empty($criteria)) {
$children = new ArrayCollection();
foreach ($this->children as $child) {
$add = true;
if (isset($criteria['visible']) && $child->getIsVisible() !== $criteria['visible']) {
$add = false;
}
if ($add) {
$children->add($child);
}
}
return $children;
}
return $this->children;
}
public function addChild(Node $child): self
{
if (!$this->children->contains($child)) {
$this->children[] = $child;
$child->setParent($this);
}
return $this;
}
public function removeChild(Node $child): self
{
if ($this->children->removeElement($child)) {
// set the owning side to null (unless already changed)
if ($child->getParent() === $this) {
$child->setParent(null);
}
}
return $this;
}
public function getAllChildren(array $criteria = []): ArrayCollection
{
$children = [];
$getChildren = function (Node $node) use (&$children, &$getChildren) {
foreach ($node->getChildren() as $nodeChildren) {
$children[] = $nodeChildren;
$getChildren($nodeChildren);
}
};
$getChildren($this);
usort($children, function ($a, $b) {
return $a->getTreeLeft() < $b->getTreeLeft() ? -1 : 1;
});
if (!empty($criteria)) {
foreach ($children as $key => $child) {
if (isset($criteria['visible']) && $child->getIsVisible() !== $criteria['visible']) {
unset($children[$key]);
}
}
}
return new ArrayCollection($children);
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(?string $label): self
{
$this->label = $label;
return $this;
}
public function getUrl(): ?string
{
return $this->url;
}
public function setUrl(?string $url): self
{
$this->url = $url;
return $this;
}
public function hasExternalUrl(): bool
{
$string = u($this->getUrl());
return $string->startsWith('http://') || $string->startsWith('https://');
}
public function hasAppUrl(): bool
{
$string = u($this->getUrl());
foreach (['tel:', 'fax:', 'mailto:'] as $prefix) {
if ($string->startsWith($prefix)) {
return true;
}
}
return false;
}
public function getIsVisible(): ?bool
{
return $this->isVisible;
}
public function setIsVisible(bool $isVisible): self
{
$this->isVisible = $isVisible;
return $this;
}
public function getDisableUrl(): ?bool
{
return $this->disableUrl;
}
public function setDisableUrl(bool $disableUrl): self
{
$this->disableUrl = $disableUrl;
return $this;
}
public function getTreeLabel()
{
$prefix = str_repeat('-', ($this->getTreeLevel() - 1) * 5);
return trim($prefix.' '.$this->getLabel());
}
public function getPage(): ?Page
{
if ($this->getAliasNode()) {
return $this->getAliasNode()->getPage();
}
return $this->page;
}
public function setPage(?Page $page): self
{
$this->page = $page;
return $this;
}
public function getRouteName(): string
{
if ($this->getAliasNode()) {
return $this->getAliasNode()->getRouteName();
}
return $this->getMenu()->getRouteName().'_'.($this->getCode() ? $this->getCode() : $this->getId());
}
public function getCode(): ?string
{
if ($this->getAliasNode()) {
return $this->getAliasNode()->getCode();
}
return $this->code;
}
public function setCode(?string $code): self
{
$this->code = $code;
return $this;
}
public function getParameters(): ?array
{
if ($this->getAliasNode()) {
return $this->getAliasNode()->getParameters();
}
if (!is_array($this->parameters)) {
$this->parameters = [];
}
return $this->parameters;
}
public function setParameters(array $parameters): self
{
$this->parameters = $parameters;
return $this;
}
public function getAttributes(): ?array
{
if (!is_array($this->attributes)) {
$this->attributes = [];
}
return $this->attributes;
}
public function setAttributes(array $attributes): self
{
$this->attributes = $attributes;
return $this;
}
public function getController(): ?string
{
if ($this->getAliasNode()) {
return $this->getAliasNode()->getController();
}
return $this->controller;
}
public function setController(?string $controller): self
{
$this->controller = $controller;
return $this;
}
public function getSitemapParameters(): ?array
{
if ($this->getAliasNode()) {
return $this->getAliasNode()->getSitemapParameters();
}
if (!is_array($this->sitemapParameters)) {
$this->sitemapParameters = [
'isVisible' => false,
'priority' => 0,
'changeFrequency' => 'daily',
];
}
return $this->sitemapParameters;
}
public function setSitemapParameters(?array $sitemapParameters): self
{
$this->sitemapParameters = $sitemapParameters;
return $this;
}
public function getAliasNode(): ?self
{
return $this->aliasNode;
}
public function setAliasNode(?self $aliasNode): self
{
$this->aliasNode = $aliasNode;
return $this;
}
/**
* @return Collection|self[]
*/
public function getAliasNodes(): Collection
{
return $this->aliasNodes;
}
public function addAliasNode(self $aliasNode): self
{
if (!$this->aliasNodes->contains($aliasNode)) {
$this->aliasNodes[] = $aliasNode;
$aliasNode->setAliasNode($this);
}
return $this;
}
public function removeAliasNode(self $aliasNode): self
{
if ($this->aliasNodes->removeElement($aliasNode)) {
// set the owning side to null (unless already changed)
if ($aliasNode->getAliasNode() === $this) {
$aliasNode->setAliasNode(null);
}
}
return $this;
}
public function getContentType(): ?string
{
return $this->contentType;
}
public function setContentType(?string $contentType): self
{
$this->contentType = $contentType;
return $this;
}
public function getEnableAnalytics(): ?bool
{
return $this->enableAnalytics;
}
public function setEnableAnalytics(bool $enableAnalytics): self
{
$this->enableAnalytics = $enableAnalytics;
return $this;
}
/**
* @return Collection|View[]
*/
public function getAnalyticViews(): Collection
{
return $this->analyticViews;
}
public function addAnalyticView(View $view): self
{
if (!$this->analyticViews->contains($view)) {
$this->analyticViews[] = $view;
$view->setNode($this);
}
return $this;
}
public function removeAnalyticView(View $view): self
{
if ($this->analyticViews->removeElement($view)) {
// set the owning side to null (unless already changed)
if ($view->getNode() === $this) {
$view->setNode(null);
}
}
return $this;
}
/**
* @return Collection|Referer[]
*/
public function getAnalyticReferers(): Collection
{
return $this->analyticReferers;
}
public function addAnalyticReferer(Referer $referer): self
{
if (!$this->analyticReferers->contains($referer)) {
$this->analyticReferers[] = $referer;
$referer->setNode($this);
}
return $this;
}
public function removeAnalyticReferer(Referer $referer): self
{
if ($this->analyticReferers->removeElement($referer)) {
// set the owning side to null (unless already changed)
if ($referer->getNode() === $this) {
$referer->setNode(null);
}
}
return $this;
}
public function getSecurityRoles(): array
{
return !is_array($this->securityRoles) ? [] : $this->securityRoles;
}
public function setSecurityRoles(array $securityRoles): self
{
$this->securityRoles = $securityRoles;
return $this;
}
public function getSecurityOperator(): ?string
{
return $this->securityOperator;
}
public function setSecurityOperator(string $securityOperator): self
{
$this->securityOperator = $securityOperator;
return $this;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Core\Entity\Site\Page;
use App\Core\Doctrine\Timestampable;
use App\Core\Repository\Site\Page\BlockRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=BlockRepository::class)
* @ORM\DiscriminatorColumn(name="class_key", type="string")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\HasLifecycleCallbacks
*/
class Block
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $name;
/**
* @ORM\Column(type="text", nullable=true)
*/
protected $value;
/**
* @ORM\ManyToOne(targetEntity=Page::class, inversedBy="blocks")
* @ORM\JoinColumn(onDelete="CASCADE")
*/
protected $page;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getValue()
{
return $this->value;
}
public function setValue($value): self
{
$this->value = $value;
return $this;
}
public function getPage(): ?Page
{
return $this->page;
}
public function setPage(?Page $page): self
{
$this->page = $page;
return $this;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class ChoiceBlock extends Block
{
public function getValue()
{
return json_decode(parent::getValue(), true);
}
public function setValue($value): self
{
return parent::setValue(json_encode($value));
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class CollectionBlock extends Block
{
public function getValue()
{
return json_decode(parent::getValue(), true);
}
public function setValue($value): self
{
return parent::setValue(json_encode($value));
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Core\Entity\Site\Page;
use App\Core\File\FileAttribute;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
/**
* @ORM\Entity
*/
class FileBlock extends Block
{
public function getValue()
{
return FileAttribute::handleFile(parent::getValue());
}
public function setValue($value): self
{
if ($this->getValue() instanceof File && null === $value) {
return $this;
}
return parent::setValue($value);
}
}

View File

@ -0,0 +1,276 @@
<?php
namespace App\Core\Entity\Site\Page;
use App\Core\Doctrine\Timestampable;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Node;
use App\Core\Repository\Site\Page\PageRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\File\File;
use App\Core\File\FileAttribute;
/**
* @ORM\Entity(repositoryClass=PageRepository::class)
* @ORM\DiscriminatorColumn(name="class_key", type="string")
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\HasLifecycleCallbacks
*/
class Page implements EntityInterface
{
use Timestampable;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $name;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $template;
/**
* @ORM\OneToMany(targetEntity=Block::class, mappedBy="page", cascade={"persist"})
*/
protected $blocks;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $metaTitle;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $metaDescription;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $ogTitle;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $ogDescription;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $ogImage;
/**
* @ORM\OneToMany(targetEntity=Node::class, mappedBy="page")
*/
protected $nodes;
public function __construct()
{
$this->blocks = new ArrayCollection();
$this->nodes = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getTemplate(): ?string
{
return $this->template;
}
public function setTemplate(?string $template): self
{
$this->template = $template;
return $this;
}
/**
* @return Collection|Block[]
*/
public function getBlocks(): Collection
{
return $this->blocks;
}
public function addBlock(Block $block): self
{
if (!$this->blocks->contains($block)) {
$this->blocks[] = $block;
$block->setPage($this);
}
return $this;
}
public function removeBlock(Block $block): self
{
if ($this->blocks->removeElement($block)) {
// set the owning side to null (unless already changed)
if ($block->getPage() === $this) {
$block->setPage(null);
}
}
return $this;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
public function getBlock($name, string $className = null)
{
foreach ($this->getBlocks() as $block) {
if ($block->getName() === $name) {
return $block;
}
}
if ($className) {
$block = new $className();
} else {
$block = new Block();
}
$block->setName($name);
$block->setPage($this);
return $block;
}
public function setBlock(Block $block): self
{
foreach ($this->blocks->toArray() as $key => $value) {
if ($value->getName() === $block->getName()) {
$this->blocks->remove($key);
$this->blocks->add($block);
return $this;
}
}
$this->blocks->add($block);
return $this;
}
public function getMetaTitle(): ?string
{
return $this->metaTitle;
}
public function setMetaTitle(?string $metaTitle): self
{
$this->metaTitle = $metaTitle;
return $this;
}
public function getMetaDescription(): ?string
{
return $this->metaDescription;
}
public function setMetaDescription(?string $metaDescription): self
{
$this->metaDescription = $metaDescription;
return $this;
}
public function getOgTitle(): ?string
{
return $this->ogTitle;
}
public function setOgTitle(?string $ogTitle): self
{
$this->ogTitle = $ogTitle;
return $this;
}
public function getOgDescription(): ?string
{
return $this->ogDescription;
}
public function setOgDescription(?string $ogDescription): self
{
$this->ogDescription = $ogDescription;
return $this;
}
public function getOgImage()
{
return FileAttribute::handleFile($this->ogImage);
}
public function setOgImage($ogImage): self
{
if (null !== $this->ogImage && null === $ogImage) {
return $this;
}
$this->ogImage = $ogImage;
return $this;
}
/**
* @return Collection|Node[]
*/
public function getNodes(): Collection
{
return $this->nodes;
}
public function addNode(Node $node): self
{
if (!$this->nodes->contains($node)) {
$this->nodes[] = $node;
$node->setPage($this);
}
return $this;
}
public function removeNode(Node $node): self
{
if ($this->nodes->removeElement($node)) {
// set the owning side to null (unless already changed)
if ($node->getPage() === $this) {
$node->setPage(null);
}
}
return $this;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Core\Entity\Site\Page;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class TextBlock extends Block
{
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Core\Event\Account;
use App\Entity\User;
use Symfony\Contracts\EventDispatcher\Event;
/**
* class PasswordRequestEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PasswordRequestEvent extends Event
{
const EVENT = 'account_event.password_request';
protected User $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function getUser(): USer
{
return $this->user;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Core\Event\EntityManager;
use App\Core\Entity\EntityInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* class EntityManagerEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class EntityManagerEvent extends Event
{
const CREATE_EVENT = 'entity_manager_event.create';
const UPDATE_EVENT = 'entity_manager_event.update';
const DELETE_EVENT = 'entity_manager_event.delete';
const PRE_CREATE_EVENT = 'entity_manager_event.pre_create';
const PRE_UPDATE_EVENT = 'entity_manager_event.pre_update';
const PRE_DELETE_EVENT = 'entity_manager_event.pre_delete';
protected EntityInterface $entity;
public function __construct(EntityInterface $entity)
{
$this->entity = $entity;
}
public function getEntity(): EntityInterface
{
return $this->entity;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Core\Event\Page;
use Symfony\Contracts\EventDispatcher\Event;
use App\Core\Entity\Site\Page\Page;
/**
* class PageEditEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageEditEvent extends Event
{
const FORM_INIT_EVENT = 'page_edit_event.form_init';
protected Page $page;
protected array $pageBuilderOptions = [];
public function __construct(Page $page)
{
$this->page = $page;
}
public function getPage()
{
return $this->page;
}
public function getPageBuilderOptions(): array
{
return $this->pageBuilderOptions;
}
public function addPageBuilderOptions(array $options): self
{
$this->pageBuilderOptions = array_merge(
$this->pageBuilderOptions,
$options
);
return $this;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Core\Event\Setting;
use Symfony\Contracts\EventDispatcher\Event;
/**
* class NavigationSettingEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NavigationSettingEvent extends Event
{
const INIT_EVENT = 'navigation_setting_event.init';
const FORM_INIT_EVENT = 'navigation_setting_event.form_init';
protected $data;
public function __construct($data = null)
{
$this->data = $data;
}
public function getData()
{
return $this->data;
}
public function setOption(string $key, $value): self
{
$this->data['options'][$key] = $value;
return $this;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Core\Event\Setting;
use Symfony\Contracts\EventDispatcher\Event;
/**
* class SettingEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class SettingEvent extends Event
{
const INIT_EVENT = 'setting_event.init';
const FORM_INIT_EVENT = 'setting_event.form_init';
protected $data;
public function __construct($data = null)
{
$this->data = $data;
}
public function getData()
{
return $this->data;
}
public function setOption(string $key, $value): self
{
$this->data['options'][$key] = $value;
return $this;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Core\Event\Task;
use Symfony\Contracts\EventDispatcher\Event;
/**
* class TaskInitEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class TaskInitEvent extends Event
{
const INIT_EVENT = 'task_event.init';
protected array $tasks = [];
public function getTasks(): array
{
usort($this->tasks, function ($t1, $t2) {
return $t1['section'] != $t2['section'];
});
return $this->tasks;
}
public function addTask(string $task, string $section, string $label): self
{
$this->tasks[$task] = [
'label' => $label,
'section' => $section,
'task' => $task,
];
return $this;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Core\Event\Task;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Contracts\EventDispatcher\Event;
/**
* class TaskRunRequestedEvent.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class TaskRunRequestedEvent extends Event
{
const RUN_REQUEST_EVENT = 'task_event.run_request';
protected string $task;
protected InputBag $parameters;
protected BufferedOutput $output;
public function __construct(string $task, InputBag $parameters, BufferedOutput $output)
{
$this->task = $task;
$this->parameters = $parameters;
$this->output = $output;
}
public function getTask(): string
{
return $this->task;
}
public function getParameters(): ParameterBagInterface
{
return $this->parameters;
}
public function getOutput(): BufferedOutput
{
return $this->output;
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace App\Core\EventListener;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Node;
use App\Core\Factory\Analytic\RefererFactory;
use App\Core\Factory\Analytic\ViewFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Analytic\RefererRepositoryQuery;
use App\Core\Repository\Analytic\ViewRepositoryQuery;
use App\Core\Repository\Site\NodeRepository;
use DeviceDetector\Cache\PSR6Bridge;
use DeviceDetector\DeviceDetector;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* class AnalyticListener.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class AnalyticListener
{
protected NodeRepository $nodeRepository;
protected ViewRepositoryQuery $viewRepositoryQuery;
protected ViewFactory $viewFactory;
protected RefererRepositoryQuery $refererRepositoryQuery;
protected RefererFactory $refererFactory;
protected EntityManager $manager;
protected DeviceDetector $deviceDetector;
protected Request $request;
protected Node $node;
public function __construct(
NodeRepository $nodeRepository,
ViewRepositoryQuery $viewRepositoryQuery,
ViewFactory $viewFactory,
RefererRepositoryQuery $refererRepositoryQuery,
RefererFactory $refererFactory,
EntityManager $manager
) {
$this->nodeRepository = $nodeRepository;
$this->viewRepositoryQuery = $viewRepositoryQuery;
$this->viewFactory = $viewFactory;
$this->refererRepositoryQuery = $refererRepositoryQuery;
$this->refererFactory = $refererFactory;
$this->manager = $manager;
$this->createDeviceDetector();
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->has('_node')) {
return;
}
$this->deviceDetector->setUserAgent($request->headers->get('user-agent'));
$this->deviceDetector->parse();
if ($this->deviceDetector->isBot()) {
return;
}
$node = $this->nodeRepository->findOneBy([
'id' => $request->attributes->get('_node'),
'enableAnalytics' => true,
]);
if (!$node) {
return;
}
$this->node = $node;
$this->request = $request;
$this->createView();
$this->createReferer();
}
protected function createDeviceDetector()
{
$cache = new ApcuAdapter();
$this->deviceDetector = new DeviceDetector();
$this->deviceDetector->setCache(new PSR6Bridge($cache));
}
protected function createView()
{
$entity = $this->viewRepositoryQuery->create()
->filterByRequest($this->request)
->andWhere('.date=CURRENT_DATE()')
->findOne()
;
if (!$entity) {
$entity = $this->viewFactory->create($this->node, $this->request->getPathInfo());
}
$entity->addView();
if ($this->deviceDetector->isDesktop()) {
$entity->addDesktopView();
} elseif ($this->deviceDetector->isMobile()) {
$entity->addMobileView();
}
$this->save($entity);
}
protected function createReferer()
{
if (!$this->request->headers->has('referer')) {
return;
}
$referer = $this->request->headers->get('referer');
if (!filter_var($referer, FILTER_VALIDATE_URL)) {
return;
}
if (!in_array(parse_url($referer, PHP_URL_SCHEME), ['http', 'https'])) {
return;
}
$entity = $this->refererRepositoryQuery->create()
->filterByRequest($this->request)
->andWhere('.date=CURRENT_DATE()')
->findOne()
;
if (!$entity) {
$entity = $this->refererFactory->create($this->node, $referer);
}
$entity->addView();
$this->save($entity);
}
protected function save(EntityInterface $entity)
{
if ($entity->getId()) {
$this->manager->update($entity);
} else {
$this->manager->create($entity);
}
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Core\EventListener;
use App\Core\Repository\RedirectRepositoryQuery;
use App\Core\Router\RedirectMatcher;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use App\Core\Router\RedirectBuilder;
/**
* class RedirectListener.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class RedirectListener
{
protected RedirectMatcher $matcher;
protected RedirectBuilder $builder;
protected RedirectRepositoryQuery $repository;
public function __construct(RedirectMatcher $matcher, RedirectBuilder $builder, RedirectRepositoryQuery $repository)
{
$this->matcher = $matcher;
$this->builder = $builder;
$this->repository = $repository;
}
public function onKernelException(ExceptionEvent $event)
{
$request = $event->getRequest();
if (!$event->getThrowable() instanceof NotFoundHttpException) {
return;
}
$redirects = $this->repository
->orderBy('.sortOrder')
->where('.isEnabled=1')
->find()
;
foreach ($redirects as $redirect) {
if ($this->matcher->match($redirect, $event->getRequest()->getUri())) {
$event->setResponse($this->builder->buildResponse($redirect, $event->getRequest()));
}
}
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Core\EventSubscriber\Account;
use App\Core\Event\Account\PasswordRequestEvent;
use App\Core\Manager\EntityManager;
use App\Core\Notification\MailNotifier;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* class EventListener.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PasswordRequestEventSubscriber implements EventSubscriberInterface
{
protected MailNotifier $notifier;
protected UrlGeneratorInterface $urlGenerator;
protected EntityManager $entityManager;
protected TokenGeneratorInterface $tokenGenerator;
protected TranslatorInterface $translator;
public function __construct(
MailNotifier $notifier,
UrlGeneratorInterface $urlGenerator,
EntityManager $entityManager,
TokenGeneratorInterface $tokenGenerator,
TranslatorInterface $translator
) {
$this->notifier = $notifier;
$this->urlGenerator = $urlGenerator;
$this->entityManager = $entityManager;
$this->tokenGenerator = $tokenGenerator;
$this->translator = $translator;
}
public static function getSubscribedEvents()
{
return [
PasswordRequestEvent::EVENT => 'onRequest',
];
}
public function onRequest(PasswordRequestEvent $event)
{
$user = $event->getUser();
$user->setConfirmationToken($this->tokenGenerator->generateToken());
$user->setPasswordRequestedAt(new \DateTime('now'));
$this->entityManager->update($user);
$this->notifier
->setSubject($this->translator->trans('Mot de passe perdu'))
->addRecipient($user->getEmail())
->notify('@Core/mail/account/resetting_request.html.twig', [
'reseting_update_link' => $this->urlGenerator->generate(
'auth_resetting_update',
[
'token' => $user->getConfirmationToken(),
],
UrlGeneratorInterface::ABSOLUTE_URL
),
])
;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Core\EventSubscriber;
use App\Core\Event\EntityManager\EntityManagerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* class EntityManagerEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class EntityManagerEventSubscriber implements EventSubscriberInterface
{
protected static int $priority = 0;
public static function getSubscribedEvents()
{
return [
EntityManagerEvent::CREATE_EVENT => ['onCreate', self::$priority],
EntityManagerEvent::UPDATE_EVENT => ['onUpdate', self::$priority],
EntityManagerEvent::DELETE_EVENT => ['onDelete', self::$priority],
EntityManagerEvent::PRE_CREATE_EVENT => ['onPreCreate', self::$priority],
EntityManagerEvent::PRE_UPDATE_EVENT => ['onPreUpdate', self::$priority],
EntityManagerEvent::PRE_DELETE_EVENT => ['onPreDelete', self::$priority],
];
}
public function onCreate(EntityManagerEvent $event)
{
}
public function onUpdate(EntityManagerEvent $event)
{
}
public function onDelete(EntityManagerEvent $event)
{
}
public function onPreCreate(EntityManagerEvent $event)
{
}
public function onPreUpdate(EntityManagerEvent $event)
{
}
public function onPreDelete(EntityManagerEvent $event)
{
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Core\EventSubscriber;
use App\Core\Event\Setting\NavigationSettingEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* class NavigationSettingEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class NavigationSettingEventSubscriber implements EventSubscriberInterface
{
protected static int $priority = 0;
public static function getSubscribedEvents()
{
return [
NavigationSettingEvent::INIT_EVENT => ['onInit', self::$priority],
NavigationSettingEvent::FORM_INIT_EVENT => ['onFormInit', self::$priority],
];
}
public function onInit(NavigationSettingEvent $event)
{
}
public function onFormInit(NavigationSettingEvent $event)
{
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Core\EventSubscriber;
use App\Core\Repository\Site\NodeRepository;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
/**
* class RequestSecurityEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class RequestSecurityEventSubscriber implements EventSubscriberInterface
{
protected NodeRepository $nodeRepository;
protected AuthorizationChecker $authorizationChecker;
public function __construct(NodeRepository $nodeRepository, ContainerInterface $container)
{
$this->nodeRepository = $nodeRepository;
$this->authorizationChecker = $container->get('security.authorization_checker');
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->has('_node')) {
return;
}
$node = $this->nodeRepository->findOneBy([
'id' => $request->attributes->get('_node'),
]);
$roles = $node->getSecurityRoles();
if (empty($roles)) {
return;
}
$operator = $node->getSecurityOperator();
$exception = new AccessDeniedException('Access denied.');
foreach ($roles as $role) {
$isGranted = $this->authorizationChecker->isGranted($role);
if ('or' === $operator && $isGranted) {
return;
}
if ('and' === $operator && !$isGranted) {
throw $exception;
}
}
throw $exception;
}
public static function getSubscribedEvents(): array
{
return [
RequestEvent::class => ['onKernelRequest', 1],
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Core\EventSubscriber;
use App\Core\Event\Setting\SettingEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* class SettingEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class SettingEventSubscriber implements EventSubscriberInterface
{
protected static int $priority = 0;
public static function getSubscribedEvents()
{
return [
SettingEvent::INIT_EVENT => ['onInit', self::$priority],
SettingEvent::FORM_INIT_EVENT => ['onFormInit', self::$priority],
];
}
public function onInit(SettingEvent $event)
{
}
public function onFormInit(SettingEvent $event)
{
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Core\EventSubscriber\Site;
use App\Core\Site\SiteRequest;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpFoundation\Response;
use function Symfony\Component\String\u;
class ForcedDomainEventSubscriber implements EventSubscriberInterface
{
protected SiteRequest $siteRequest;
public function __construct(SiteRequest $siteRequest)
{
$this->siteRequest = $siteRequest;
}
public function onKernelResponse(ResponseEvent $event)
{
$navigation = $this->siteRequest->getNavigation();
if (!$navigation) {
return;
}
if (!$navigation->getForceDomain()) {
return;
}
if ($navigation->getDomain() === $this->siteRequest->getDomain()) {
return;
}
$uri = u($this->siteRequest->getUri())
->replace(
'://'.$this->siteRequest->getDomain(),
'://'.$navigation->getDomain()
);
$event->getResponse()->headers->set('Location', $uri);
$event->getResponse()->setStatusCode(Response::HTTP_MOVED_PERMANENTLY);
}
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => [['onKernelResponse', 20]],
];
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Core\EventSubscriber\Site;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Menu;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Factory\Site\NodeFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;
use App\Core\Slugify\CodeSlugify;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* class MenuEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class MenuEventSubscriber extends EntityManagerEventSubscriber
{
protected NodeFactory $nodeFactory;
protected NodeRepository $nodeRepository;
protected EntityManager $entityManager;
protected CodeSlugify $slugify;
protected SymfonyCacheManager $cacheManager;
protected TranslatorInterface $translation;
public function __construct(
NodeFactory $nodeFactory,
NodeRepository $nodeRepository,
EntityManager $entityManager,
CodeSlugify $slugify,
SymfonyCacheManager $cacheManager,
TranslatorInterface $translator
) {
$this->nodeFactory = $nodeFactory;
$this->nodeRepository = $nodeRepository;
$this->entityManager = $entityManager;
$this->slugify = $slugify;
$this->cacheManager = $cacheManager;
$this->translator = $translator;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Menu;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$menu = $event->getEntity();
$menu->setCode($this->slugify->slugify($menu->getCode()));
}
public function onCreate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$menu = $event->getEntity();
if (count($menu->getNodes()) > 2) {
return;
}
$rootNode = $menu->getRootNode();
if (!$rootNode) {
$rootNode = $this->nodeFactory->create($menu);
}
$childNode = $this->nodeFactory->create($menu, '/');
$childNode
->setParent($rootNode)
->setLabel($this->translator->trans('First element'))
;
$menu->setRootNode($rootNode);
$this->entityManager->getEntityManager()->persist($rootNode);
$this->entityManager->getEntityManager()->persist($childNode);
$this->entityManager->getEntityManager()->persist($menu);
$this->entityManager->flush();
$this->nodeRepository->persistAsFirstChild($childNode, $rootNode);
$this->cacheManager->cleanRouting();
}
public function onUpdate(EntityManagerEvent $event)
{
return $this->onCreate($event);
}
public function onPreCreate(EntityManagerEvent $event)
{
return $this->onPreUpdate($event);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Core\EventSubscriber\Site;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Navigation;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Manager\EntityManager;
use App\Core\Slugify\CodeSlugify;
/**
* class NavigationEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NavigationEventSubscriber extends EntityManagerEventSubscriber
{
public function __construct(
EntityManager $entityManager,
CodeSlugify $slugify
) {
$this->entityManager = $entityManager;
$this->slugify = $slugify;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Navigation;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$menu = $event->getEntity();
$menu->setCode($this->slugify->slugify($menu->getCode()));
}
public function onPreCreate(EntityManagerEvent $event)
{
return $this->onPreUpdate($event);
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace App\Core\EventSubscriber\Site;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Node;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Factory\Site\NodeFactory;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;
use App\Core\Slugify\CodeSlugify;
use App\Core\Slugify\RouteParameterSlugify;
use App\Core\Slugify\Slugify;
use Symfony\Component\HttpKernel\KernelInterface;
use function Symfony\Component\String\u;
/**
* class NodeEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NodeEventSubscriber extends EntityManagerEventSubscriber
{
protected NodeFactory $nodeFactory;
protected EntityManager $entityManager;
protected KernelInterface $kernel;
protected Slugify $slugify;
protected CodeSlugify $codeSlugify;
protected RouteParameterSlugify $routeParameterSlugify;
public function __construct(
NodeFactory $nodeFactory,
NodeRepository $nodeRepository,
EntityManager $entityManager,
Slugify $slugify,
CodeSlugify $codeSlugify,
RouteParameterSlugify $routeParameterSlugify
) {
$this->nodeFactory = $nodeFactory;
$this->nodeRepository = $nodeRepository;
$this->entityManager = $entityManager;
$this->slugify = $slugify;
$this->codeSlugify = $codeSlugify;
$this->routeParameterSlugify = $routeParameterSlugify;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Node;
}
public function onPreCreate(EntityManagerEvent $event)
{
return $this->onPreUpdate($event);
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$node = $event->getEntity();
$node->setCode($this->codeSlugify->slugify($node->getCode()));
if ($node->getDisableUrl()) {
$node->setUrl(null);
} else {
if ($node->getUrl()) {
$generatedUrl = $node->getUrl();
} else {
$path = [];
$parent = $node->getParent();
if ($parent && $parent->getUrl()) {
$pPath = trim($parent->getUrl(), '/');
if ($pPath) {
$path[] = $pPath;
}
}
$path[] = $this->slugify->slugify($node->getLabel());
$generatedUrl = '/'.implode('/', $path);
}
if ('/' !== $generatedUrl) {
$generatedUrl = rtrim($generatedUrl, '/');
}
$parameters = $node->getParameters();
$routeParameters = [];
foreach ($parameters as $key => $parameter) {
$parameter['name'] = $this->routeParameterSlugify->slugify($parameter['name']);
$routeParameter = sprintf('{%s}', $parameter['name']);
$regex = '/'.preg_quote($routeParameter).'/';
$routeParameters[] = $parameter['name'];
if (!preg_match($regex, $generatedUrl)) {
$generatedUrl .= '/'.$routeParameter;
}
$parameters[$key] = $parameter;
}
preg_match_all('/\{(.*)\}/isU', $generatedUrl, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if (!in_array($match[1], $routeParameters)) {
$parameters[] = [
'name' => $this->routeParameterSlugify->slugify($match[1]),
'defaultValue' => null,
'requirement' => null,
];
}
}
$node->setParameters($parameters);
$urlExists = $this->nodeRepository->urlExists($generatedUrl, $node);
if ($urlExists) {
$number = 1;
while ($this->nodeRepository->urlExists($generatedUrl.'-'.$number, $node)) {
++$number;
}
$generatedUrl = $generatedUrl.'-'.$number;
}
if (
!u($generatedUrl)->startsWith('https://')
&& !u($generatedUrl)->startsWith('http://')
&& !u($generatedUrl)->startsWith('tel:')
&& !u($generatedUrl)->startsWith('mailto:')
&& !u($generatedUrl)->startsWith('fax:')
) {
$generatedUrl = '/'.$generatedUrl;
$generatedUrl = preg_replace('#/{2,}#', '/', $generatedUrl);
}
$node->setUrl($generatedUrl);
}
$attributes = $node->getAttributes();
$realAttributes = [];
foreach ($attributes as $key => $attribute) {
$realAttributes[$this->routeParameterSlugify->slugify($attribute['label'])] = $attribute;
}
$node->setAttributes($realAttributes);
}
public function onDelete(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$menu = $event->getEntity()->getMenu();
$rootNode = $menu->getRootNode();
if (0 !== count($rootNode->getChildren())) {
return;
}
$childNode = $this->nodeFactory->create($menu);
$childNode
->setParent($rootNode)
->setLabel('Premier élément')
;
$this->entityManager->update($rootNode, false);
$this->entityManager->create($childNode, false);
$this->nodeRepository->persistAsFirstChild($childNode, $rootNode);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Core\EventSubscriber\Site\Page;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Page\FileBlock;
use App\Core\Entity\Site\Page\Page;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Form\FileUploadHandler;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* class BlockEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class BlockEventSubscriber extends EntityManagerEventSubscriber
{
protected FileUploadHandler $fileUpload;
public function __construct(FileUploadHandler $fileUpload)
{
$this->fileUpload = $fileUpload;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Page;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
foreach ($event->getEntity()->getBlocks() as $block) {
if ($block instanceof FileBlock) {
if ($block->getValue() instanceof UploadedFile) {
$directory = 'uploads/page/block';
$this->fileUpload->handleForm(
$block->getValue(),
$directory,
function ($filename) use ($block, $directory) {
$block->setValue($directory.'/'.$filename);
}
);
}
}
}
}
public function onPreCreate(EntityManagerEvent $event)
{
return $this->onPreUpdate($event);
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Core\EventSubscriber\Site\Page;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Page\Page;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use App\Core\Form\FileUploadHandler;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* class PageEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageEventSubscriber extends EntityManagerEventSubscriber
{
protected FileUploadHandler $fileUpload;
public function __construct(FileUploadHandler $fileUpload)
{
$this->fileUpload = $fileUpload;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Page;
}
public function onPreUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$page = $event->getEntity();
if ($page->getOgImage() instanceof UploadedFile) {
$directory = 'uploads/page/ogImage';
$this->fileUpload->handleForm(
$page->getOgImage(),
$directory,
function ($filename) use ($page, $directory) {
$page->setOgImage($directory.'/'.$filename);
}
);
}
}
public function onPreCreate(EntityManagerEvent $event)
{
return $this->onPreUpdate($event);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Core\EventSubscriber\Site;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Entity\EntityInterface;
use App\Core\Entity\Site\Menu;
use App\Core\Entity\Site\Navigation;
use App\Core\Entity\Site\Node;
use App\Core\Event\EntityManager\EntityManagerEvent;
use App\Core\EventSubscriber\EntityManagerEventSubscriber;
use Symfony\Component\HttpKernel\KernelInterface;
/**
* class SiteEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class SiteEventSubscriber extends EntityManagerEventSubscriber
{
protected KernelInterface $kernel;
protected SymfonyCacheManager $cacheManager;
public function __construct(KernelInterface $kernel, SymfonyCacheManager $cacheManager)
{
$this->kernel = $kernel;
$this->cacheManager = $cacheManager;
}
public function support(EntityInterface $entity)
{
return $entity instanceof Node || $entity instanceof Menu || $entity instanceof Navigation;
}
public function onUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$this->cacheManager->cleanRouting();
}
public function onCreate(EntityManagerEvent $event)
{
return $this->onUpdate($event);
}
public function onDelete(EntityManagerEvent $event)
{
return $this->onUpdate($event);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Core\EventSubscriber\Task;
use App\Core\Cache\SymfonyCacheManager;
use App\Core\Event\Task\TaskInitEvent;
use App\Core\Event\Task\TaskRunRequestedEvent;
/**
* class CacheCleanTaskEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CacheCleanTaskEventSubscriber extends TaskEventSubscriber
{
protected SymfonyCacheManager $cacheManager;
public function __construct(SymfonyCacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
}
public function onInit(TaskInitEvent $event)
{
$event->addTask('cache:clear', '♻️ Cache', 'Clean all cache');
}
public function onRunRequest(TaskRunRequestedEvent $event)
{
if ('cache:clear' !== $event->getTask()) {
return;
}
$this->cacheManager->cleanAll($event->getOutput());
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Core\EventSubscriber\Task;
use App\Core\Event\Task\TaskInitEvent;
use App\Core\Event\Task\TaskRunRequestedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* class TaskEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
abstract class TaskEventSubscriber implements EventSubscriberInterface
{
protected static int $priority = 0;
public static function getSubscribedEvents()
{
return [
TaskInitEvent::INIT_EVENT => ['onInit', self::$priority],
TaskRunRequestedEvent::RUN_REQUEST_EVENT => ['onRunRequest', self::$priority],
];
}
public function onInit(TaskInitEvent $event)
{
}
public function onRunRequest(TaskRunRequestedEvent $event)
{
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Core\Factory\Analytic;
use App\Core\Entity\Analytic\Referer as Entity;
use App\Core\Entity\Site\Node;
use App\Core\Factory\FactoryInterface;
class RefererFactory implements FactoryInterface
{
public function create(Node $node, string $uri): Entity
{
$entity = new Entity();
$entity
->setNode($node)
->setUri($uri)
->setDate(new \DateTime())
;
return $entity;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Core\Factory\Analytic;
use App\Core\Entity\Analytic\View as Entity;
use App\Core\Entity\Site\Node;
use App\Core\Factory\FactoryInterface;
class ViewFactory implements FactoryInterface
{
public function create(Node $node, string $path): Entity
{
$entity = new Entity();
$entity
->setNode($node)
->setPath($path)
->setDate(new \DateTime())
;
return $entity;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Core\Factory;
/**
* interface FactoryInterface.
*
* @author Simon Vieille <simon@deblan.fr>
*/
interface FactoryInterface
{
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Core\Factory;
use App\Core\Entity\FileInformation as Entity;
class FileInformationFactory implements FactoryInterface
{
public function create(string $id): Entity
{
$entity = new Entity();
$entity->setId($id);
return $entity;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Core\Factory;
use App\Core\Entity\NavigationSetting;
use App\Core\Entity\Site\Navigation;
/**
* class NavigationSettingFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NavigationSettingFactory implements FactoryInterface
{
public function create(Navigation $navigation, string $code): NavigationSetting
{
$entity = new NavigationSetting();
$entity
->setNavigation($navigation)
->setCode($code)
;
return $entity;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Core\Factory;
use App\Core\Factory\FactoryInterface;
use App\Core\Entity\Redirect as Entity;
class RedirectFactory implements FactoryInterface
{
public function create(): Entity
{
return new Entity();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Core\Factory;
use App\Core\Entity\Setting;
/**
* class SettingFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class SettingFactory implements FactoryInterface
{
public function create(string $code): Setting
{
$entity = new Setting();
$entity->setCode($code);
return $entity;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Core\Factory\Site;
use App\Core\Entity\Site\Menu;
use App\Core\Entity\Site\Navigation;
use App\Core\Factory\FactoryInterface;
/**
* class MenuFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class MenuFactory implements FactoryInterface
{
public function create(?Navigation $navigation = null): Menu
{
$entity = new Menu();
if (null !== $navigation) {
$entity->setNavigation($navigation);
}
return $entity;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Core\Factory\Site;
use App\Core\Entity\Site\Navigation;
use App\Core\Factory\FactoryInterface;
/**
* class NavigationFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NavigationFactory implements FactoryInterface
{
public function create(): Navigation
{
return new Navigation();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Core\Factory\Site;
use App\Core\Entity\Site\Menu;
use App\Core\Entity\Site\Node;
use App\Core\Factory\FactoryInterface;
/**
* class NodeFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class NodeFactory implements FactoryInterface
{
public function create(?Menu $menu = null, string $url = null): Node
{
$entity = new Node();
if (null !== $menu) {
$entity->setMenu($menu);
}
if (null !== $url) {
$entity->setUrl($url);
}
return $entity;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Core\Factory\Site\Page;
use App\Core\Entity\Site\Page\Page;
use App\Core\Factory\FactoryInterface;
/**
* class PageFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PageFactory implements FactoryInterface
{
public function create(string $className, string $name): Page
{
$entity = new $className();
$entity->setName($name);
return $entity;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Core\Factory;
use App\Entity\User;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;
/**
* class UserFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class UserFactory implements FactoryInterface
{
protected TokenGeneratorInterface $tokenGenerator;
protected UserPasswordEncoderInterface $encoder;
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public function create(?string $email = null, ?string $password = null): User
{
$entity = new User();
if (null !== $email) {
$entity->setEmail($email);
}
if (null !== $password) {
$entity->setPassword($this->encoder->encodePassword($entity, $password));
}
return $entity;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Core\File;
use Symfony\Component\HttpFoundation\File\File;
/**
* class FileAttribute.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class FileAttribute
{
public static function handleFile($attribute, string $class = null)
{
if (null === $class) {
$class = File::class;
}
if (is_string($attribute)) {
if (file_exists($attribute)) {
return new $class($attribute);
}
return null;
}
return $attribute;
}
}

View File

@ -0,0 +1,321 @@
<?php
namespace App\Core\FileManager;
use App\Core\Entity\FileInformation;
use App\Core\Factory\FileInformationFactory;
use App\Core\Form\FileUploadHandler;
use App\Core\Repository\FileInformationRepositoryQuery;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use function Symfony\Component\String\u;
/**
* class FsFileManager.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class FsFileManager
{
protected array $mimes;
protected string $path;
protected string $pathUri;
protected array $pathLocked;
protected FileUploadHandler $uploadHandler;
protected FileInformationFactory $fileInformationFactory;
protected FileInformationRepositoryQuery $fileInformationRepositoryQuery;
public function __construct(
ParameterBagInterface $params,
FileUploadHandler $uploadHandler,
FileInformationFactory $fileInformationFactory,
FileInformationRepositoryQuery $fileInformationRepositoryQuery
) {
$config = $params->get('core')['file_manager'];
$this->uploadHandler = $uploadHandler;
$this->fileInformationFactory = $fileInformationFactory;
$this->fileInformationRepositoryQuery = $fileInformationRepositoryQuery;
$this->mimes = $config['mimes'];
$this->path = $config['path'];
$this->pathUri = $this->normalizePath($config['path_uri']);
foreach ($config['path_locked'] as $k => $v) {
$config['path_locked'][$k] = sprintf('/%s/', $this->normalizePath($v));
}
$this->pathLocked = $config['path_locked'];
}
public function list(string $directory, array $options = []): array
{
$directory = $this->normalizePath($directory);
$breadcrumb = ['/'];
if ($directory) {
$breadcrumb = array_merge(
$breadcrumb,
explode('/', $directory)
);
}
$data = [
'breadcrumb' => $breadcrumb,
'parent' => dirname($directory),
'directories' => [],
'files' => [],
];
$finder = new Finder();
$finder->directories()->depth('== 0')->in($this->path.'/'.$directory);
$this->applySort($finder, $options['sort'] ?? 'name', $options['sort_direction'] ?? 'asc');
foreach ($finder as $file) {
$data['directories'][] = [
'basename' => $file->getBasename(),
'path' => $directory.'/'.$file->getBasename(),
'webPath' => $this->pathUri.'/'.$directory.'/'.$file->getBasename(),
'locked' => $this->isLocked($directory.'/'.$file->getBasename()),
'mime' => null,
];
}
$finder = new Finder();
$finder->files()->depth('== 0')->in($this->path.'/'.$directory);
$this->applySort($finder, $options['sort'] ?? 'name', $options['sort_direction'] ?? 'asc');
foreach ($finder as $file) {
$data['files'][] = [
'basename' => $file->getBasename(),
'path' => $directory,
'webPath' => $this->pathUri.'/'.$directory.'/'.$file->getBasename(),
'locked' => $this->isLocked($directory.'/'.$file->getBasename()),
'mime' => mime_content_type($file->getRealPath()),
];
}
return $data;
}
public function getSplInfo(string $path): ?SplFileInfo
{
$path = $this->normalizePath($path);
if ('' === $path) {
return new SplFileInfo($this->path, '', '');
}
$finder = new Finder();
$finder->in($this->path)
->depth('== '.substr_count($path, '/'))
->name(basename($path))
;
$dirname = dirname($path);
if ('.' === $dirname) {
$dirname = '';
}
foreach ($finder as $file) {
if ($file->getRelativePath() === $dirname) {
return $file;
}
}
return null;
}
public function getFileInformation(string $path): ?FileInformation
{
$file = $this->getSplInfo($path);
if (!$file) {
return null;
}
if ($file->isDir()) {
return null;
}
$hash = hash_file('sha384', $file->getPathName());
$info = $this->fileInformationRepositoryQuery
->where('.id = :hash')
->setParameter(':hash', $hash)
->findOne()
;
if (!$info) {
$info = $this->fileInformationFactory->create($hash);
}
return $info;
}
public function createDirectory(string $name, string $path): bool
{
$file = $this->getSplInfo($path);
if (!$file || $this->isLocked($path)) {
return false;
}
$filesystem = new Filesystem();
$path = $file->getPathname().'/'.$this->normalizePath($name);
if ($filesystem->exists($path)) {
return false;
}
$filesystem->mkdir($path, 0755);
return true;
}
public function renameDirectory(string $name, string $path): bool
{
$file = $this->getSplInfo($path);
if (!$file || $this->isLocked($path)) {
return false;
}
$filesystem = new Filesystem();
$newPath = $file->getPath().'/'.$this->normalizePath($name);
if ($filesystem->exists($newPath)) {
return false;
}
$filesystem->rename($file->getPathName(), $newPath);
return true;
}
public function renameFile(string $name, string $path, bool $keepExtension = true): bool
{
$file = $this->getSplInfo($path);
if (!$file || $this->isLocked($path)) {
return false;
}
$filesystem = new Filesystem();
$newPath = $file->getPath().'/'.$this->normalizePath($name);
if ($keepExtension && $file->getExtension()) {
$newPath .= sprintf('.%s', $file->getExtension());
}
if ($filesystem->exists($newPath)) {
return false;
}
$filesystem->rename($file->getPathName(), $newPath);
return true;
}
public function upload($files, string $path, array $fullPaths = [])
{
if ($files instanceof UploadedFile) {
$files = [$files];
}
foreach ($files as $key => $file) {
$directory = $this->path.'/'.$path;
if (isset($fullPaths[$key])) {
$directory .= '/'.trim(dirname($fullPaths[$key]), '/');
}
$this->uploadHandler->handleForm($file, $directory, null, true);
}
}
public function delete(string $path): bool
{
$file = $this->getSplInfo($path);
if ($this->isLocked($file)) {
return false;
}
if ($file) {
$filesystem = new Filesystem();
$filesystem->remove($file);
return true;
}
return false;
}
public function isLocked($path): bool
{
$file = $this->getSplInfo($path);
if (!$file) {
return false;
}
foreach ($this->pathLocked as $lock) {
if (u($file->getPathName().'/')->startsWith($lock)) {
return true;
}
}
return false;
}
public function getPath(): string
{
return $this->path;
}
public function getPathUri(): string
{
return $this->pathUri;
}
public function getMimes(): array
{
return $this->mimes;
}
public function getPathLocked(): array
{
return $this->pathLocked;
}
protected function applySort(Finder $finder, string $sort, string $direction)
{
if ('name' === $sort) {
$finder->sortByName();
} elseif ('modification_date' === $sort) {
$finder->sortByModifiedTime();
}
if ('desc' === $direction) {
$finder->reverseSorting();
}
}
protected function normalizePath(string $path): string
{
return (string) u($path)
->replace('..', '.')
->replaceMatches('#/{2,}#', '/')
->replaceMatches('#^.$#', '')
->trim('/')
->trim()
;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
class DirectoryCreateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'name',
TextType::class,
[
'label' => 'Name',
'required' => true,
'attr' => [
],
'constraints' => [
new Regex([
'pattern' => '#['.preg_quote('\\/?%*:|"<>').'\t\n\r]+#',
'match' => false,
]),
new NotBlank(),
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
]);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
class DirectoryRenameType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'name',
TextType::class,
[
'label' => 'Name',
'required' => true,
'attr' => [
],
'constraints' => [
new Regex([
'pattern' => '#['.preg_quote('\\/?%*:|"<>').'\t\n\r]+#',
'match' => false,
]),
new NotBlank(),
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
]);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class FileInformationAttributeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'label',
TextType::class,
[
'label' => 'Label',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'value',
TextType::class,
[
'label' => 'Value',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Core\Form\FileManager;
use App\Core\Entity\FileInformation;
use App\Core\Form\Type\CollectionType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FileInformationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'attributes',
CollectionType::class,
[
'entry_type' => FileInformationAttributeType::class,
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FileInformation::class,
]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Core\Form\FileManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
class FilePickerType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['pattern'] = null;
unset($view->vars['attr']['pattern']);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'file_picker';
}
}

Some files were not shown because too many files have changed in this diff Show More