diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c386363..c621ab0 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1 @@ github: yoeunes -patreon: yoeunes -ko_fi: yoeunes -open_collective: php-flasher -custom: https://www.paypal.com/paypalme/yoeunes diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..821ddf5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/php-flasher/php-flasher + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/auto_closer.yaml b/.github/workflows/auto_closer.yaml deleted file mode 100644 index f807ac5..0000000 --- a/.github/workflows/auto_closer.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Auto Closer PR - -on: - pull_request_target: - types: [opened] - -jobs: - run: - runs-on: ubuntu-latest - steps: - - uses: superbrothers/close-pull-request@v3 - with: - comment: | - Hi, thank you for your contribution. - - Unfortunately, this repository is read-only. It's a split from our main monorepo repository. - - We'd like to kindly ask you to move the contribution there - https://github.com/php-flasher/php-flasher. - - We'll check it, review it and give you feed back right way. - - Thank you. diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000..6e2076f --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/php-flasher/php-flasher + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php new file mode 100644 index 0000000..5484be8 --- /dev/null +++ b/.phpstorm.meta.php @@ -0,0 +1,8 @@ + - */ - -namespace Flasher\Symfony\Bridge; - -use Symfony\Component\HttpKernel\Kernel; - -final class Bridge -{ - /** - * @return bool - */ - public static function isLegacy() - { - return self::versionCompare('6', '<'); - } - - /** - * @param string $version - * @param string $operator - * - * @return bool - */ - public static function versionCompare($version, $operator = '=') - { - return version_compare(Kernel::VERSION, $version, $operator); - } - - /** - * @return bool - */ - public static function canLoadAliases() - { - return self::versionCompare('3.0', '>='); - } -} diff --git a/Bridge/Command/FlasherCommand.php b/Bridge/Command/FlasherCommand.php deleted file mode 100644 index 3320498..0000000 --- a/Bridge/Command/FlasherCommand.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Command; - -use Flasher\Symfony\Bridge\Bridge; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -$class = Bridge::versionCompare('6.4', '>=') - ? 'Flasher\Symfony\Bridge\Typed\Command\FlasherCommand' - : 'Flasher\Symfony\Bridge\Legacy\Command\FlasherCommand'; - -class_alias($class, 'Flasher\Symfony\Bridge\Command\FlasherCommand'); - -if (false) { /** @phpstan-ignore-line */ - abstract class FlasherCommand - { - /** - * @return int - */ - abstract protected function flasherExecute(InputInterface $input, OutputInterface $output); - } -} diff --git a/Bridge/DependencyInjection/FlasherConfiguration.php b/Bridge/DependencyInjection/FlasherConfiguration.php deleted file mode 100644 index 903cd89..0000000 --- a/Bridge/DependencyInjection/FlasherConfiguration.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\DependencyInjection; - -use Flasher\Symfony\Bridge\Bridge; - -$class = Bridge::versionCompare('6.4', '>=') - ? 'Flasher\Symfony\Bridge\Typed\DependencyInjection\FlasherConfiguration' - : 'Flasher\Symfony\Bridge\Legacy\DependencyInjection\FlasherConfiguration'; - -class_alias($class, 'Flasher\Symfony\Bridge\DependencyInjection\FlasherConfiguration'); - -if (false) { /** @phpstan-ignore-line */ - abstract class FlasherConfiguration - { - /** - * @return string - */ - abstract protected function getFlasherConfigTreeBuilder(); - } -} diff --git a/Bridge/DependencyInjection/FlasherExtension.php b/Bridge/DependencyInjection/FlasherExtension.php deleted file mode 100644 index 2eec4ad..0000000 --- a/Bridge/DependencyInjection/FlasherExtension.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\DependencyInjection; - -use Flasher\Symfony\Bridge\Bridge; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; - -$class = Bridge::isLegacy() - ? 'Flasher\Symfony\Bridge\Legacy\DependencyInjection\FlasherExtension' - : 'Flasher\Symfony\Bridge\Typed\DependencyInjection\FlasherExtension'; - -class_alias($class, 'Flasher\Symfony\Bridge\DependencyInjection\FlasherExtension'); - -if (false) { /** @phpstan-ignore-line */ - abstract class FlasherExtension extends Extension - { - /** - * @return string - */ - abstract protected function getFlasherAlias(); - } -} diff --git a/Bridge/FlasherBundle.php b/Bridge/FlasherBundle.php deleted file mode 100644 index 89c2013..0000000 --- a/Bridge/FlasherBundle.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge; - -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; - -$class = Bridge::isLegacy() - ? 'Flasher\Symfony\Bridge\Legacy\FlasherBundle' - : 'Flasher\Symfony\Bridge\Typed\FlasherBundle'; - -class_alias($class, 'Flasher\Symfony\Bridge\FlasherBundle'); - -if (false) { /** @phpstan-ignore-line */ - abstract class FlasherBundle - { - /** - * @return void - */ - protected function flasherBuild(ContainerBuilder $container) - { - } - - /** - * @return ?ExtensionInterface - */ - protected function getFlasherContainerExtension() - { - return null; - } - } -} diff --git a/Bridge/Legacy/Command/FlasherCommand.php b/Bridge/Legacy/Command/FlasherCommand.php deleted file mode 100644 index 19ab601..0000000 --- a/Bridge/Legacy/Command/FlasherCommand.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Legacy\Command; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -abstract class FlasherCommand extends Command -{ - protected function execute(InputInterface $input, OutputInterface $output) - { - return $this->flasherExecute($input, $output); - } - - /** - * @return int - */ - abstract protected function flasherExecute(InputInterface $input, OutputInterface $output); -} diff --git a/Bridge/Legacy/DependencyInjection/FlasherConfiguration.php b/Bridge/Legacy/DependencyInjection/FlasherConfiguration.php deleted file mode 100644 index ec43c69..0000000 --- a/Bridge/Legacy/DependencyInjection/FlasherConfiguration.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Legacy\DependencyInjection; - -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; - -abstract class FlasherConfiguration implements ConfigurationInterface -{ - public function getConfigTreeBuilder() - { - return $this->getFlasherConfigTreeBuilder(); - } - - /** - * @return TreeBuilder - */ - abstract public function getFlasherConfigTreeBuilder(); -} diff --git a/Bridge/Legacy/DependencyInjection/FlasherExtension.php b/Bridge/Legacy/DependencyInjection/FlasherExtension.php deleted file mode 100644 index 77ccd90..0000000 --- a/Bridge/Legacy/DependencyInjection/FlasherExtension.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Legacy\DependencyInjection; - -use Symfony\Component\HttpKernel\DependencyInjection\Extension; - -abstract class FlasherExtension extends Extension -{ - /** - * {@inheritdoc} - */ - public function getAlias() - { - return $this->getFlasherAlias(); - } - - /** - * @return string - */ - abstract protected function getFlasherAlias(); -} diff --git a/Bridge/Legacy/FlasherBundle.php b/Bridge/Legacy/FlasherBundle.php deleted file mode 100644 index c71c846..0000000 --- a/Bridge/Legacy/FlasherBundle.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Legacy; - -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; -use Symfony\Component\HttpKernel\Bundle\Bundle; - -abstract class FlasherBundle extends Bundle -{ - /** - * {@inheritdoc} - */ - public function build(ContainerBuilder $container) - { - $this->flasherBuild($container); - } - - /** - * {@inheritdoc} - */ - public function getContainerExtension() - { - return $this->getFlasherContainerExtension(); - } - - /** - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function flasherBuild(ContainerBuilder $container) - { - } - - /** - * @return ?ExtensionInterface - */ - abstract protected function getFlasherContainerExtension(); -} diff --git a/Bridge/Legacy/Twig/FlasherTwigExtension.php b/Bridge/Legacy/Twig/FlasherTwigExtension.php deleted file mode 100644 index e1bd20e..0000000 --- a/Bridge/Legacy/Twig/FlasherTwigExtension.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Legacy\Twig; - -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; - -abstract class FlasherTwigExtension extends AbstractExtension -{ - /** - * {@inheritdoc} - */ - public function getFunctions() - { - return $this->getFlasherFunctions(); - } - - /** - * @return TwigFunction[] - */ - abstract protected function getFlasherFunctions(); -} diff --git a/Bridge/Twig/FlasherTwigExtension.php b/Bridge/Twig/FlasherTwigExtension.php deleted file mode 100644 index 7d3e6c0..0000000 --- a/Bridge/Twig/FlasherTwigExtension.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Twig; - -use Flasher\Symfony\Bridge\Bridge; -use Twig\TwigFunction; - -$class = Bridge::isLegacy() - ? 'Flasher\Symfony\Bridge\Legacy\Twig\FlasherTwigExtension' - : 'Flasher\Symfony\Bridge\Typed\Twig\FlasherTwigExtension'; - -class_alias($class, 'Flasher\Symfony\Bridge\Twig\FlasherTwigExtension'); - -if (false) { /** @phpstan-ignore-line */ - abstract class FlasherTwigExtension - { - /** - * @return TwigFunction[] - */ - abstract protected function getFlasherFunctions(); - } -} diff --git a/Bridge/Typed/Command/FlasherCommand.php b/Bridge/Typed/Command/FlasherCommand.php deleted file mode 100644 index 50e99dd..0000000 --- a/Bridge/Typed/Command/FlasherCommand.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Typed\Command; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -abstract class FlasherCommand extends Command -{ - protected function execute(InputInterface $input, OutputInterface $output): int - { - return $this->flasherExecute($input, $output); - } - - /** - * @return int - */ - abstract protected function flasherExecute(InputInterface $input, OutputInterface $output); -} diff --git a/Bridge/Typed/DependencyInjection/FlasherConfiguration.php b/Bridge/Typed/DependencyInjection/FlasherConfiguration.php deleted file mode 100644 index bca3f45..0000000 --- a/Bridge/Typed/DependencyInjection/FlasherConfiguration.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Typed\DependencyInjection; - -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; - -abstract class FlasherConfiguration implements ConfigurationInterface -{ - public function getConfigTreeBuilder(): TreeBuilder - { - return $this->getFlasherConfigTreeBuilder(); - } - - /** - * @return TreeBuilder - */ - abstract public function getFlasherConfigTreeBuilder(); -} diff --git a/Bridge/Typed/DependencyInjection/FlasherExtension.php b/Bridge/Typed/DependencyInjection/FlasherExtension.php deleted file mode 100644 index d0fc1bb..0000000 --- a/Bridge/Typed/DependencyInjection/FlasherExtension.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Typed\DependencyInjection; - -use Symfony\Component\HttpKernel\DependencyInjection\Extension; - -abstract class FlasherExtension extends Extension -{ - /** - * {@inheritdoc} - */ - public function getAlias(): string - { - return $this->getFlasherAlias(); - } - - /** - * @return string - */ - abstract protected function getFlasherAlias(); -} diff --git a/Bridge/Typed/FlasherBundle.php b/Bridge/Typed/FlasherBundle.php deleted file mode 100644 index f259bf9..0000000 --- a/Bridge/Typed/FlasherBundle.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Typed; - -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; -use Symfony\Component\HttpKernel\Bundle\Bundle; - -abstract class FlasherBundle extends Bundle -{ - /** - * {@inheritdoc} - * - * @return void - */ - public function build(ContainerBuilder $container) - { - $this->flasherBuild($container); - } - - /** - * {@inheritdoc} - */ - public function getContainerExtension(): ?ExtensionInterface - { - return $this->getFlasherContainerExtension(); - } - - /** - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function flasherBuild(ContainerBuilder $container) - { - } - - /** - * @return ?ExtensionInterface - */ - abstract protected function getFlasherContainerExtension(); -} diff --git a/Bridge/Typed/Twig/FlasherTwigExtension.php b/Bridge/Typed/Twig/FlasherTwigExtension.php deleted file mode 100644 index a71547d..0000000 --- a/Bridge/Typed/Twig/FlasherTwigExtension.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - -namespace Flasher\Symfony\Bridge\Typed\Twig; - -use Twig\Extension\AbstractExtension; - -abstract class FlasherTwigExtension extends AbstractExtension -{ - /** - * {@inheritdoc} - */ - public function getFunctions(): array - { - return $this->getFlasherFunctions(); - } - - /** - * @return array - */ - abstract protected function getFlasherFunctions(); -} diff --git a/Command/InstallCommand.php b/Command/InstallCommand.php index 65ff4e1..1bdda6a 100644 --- a/Command/InstallCommand.php +++ b/Command/InstallCommand.php @@ -1,41 +1,72 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\Command; +use Flasher\Prime\Asset\AssetManagerInterface; use Flasher\Prime\Plugin\PluginInterface; -use Flasher\Symfony\Bridge\Bridge; -use Flasher\Symfony\Bridge\Command\FlasherCommand; -use Flasher\Symfony\Support\Bundle; +use Flasher\Symfony\Support\PluginBundleInterface; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\KernelInterface; -class InstallCommand extends FlasherCommand +/** + * InstallCommand - Console command for installing PHPFlasher resources. + * + * This command provides a CLI interface for installing PHPFlasher resources, + * including assets (JS and CSS files) and configuration files. It discovers + * all registered PHPFlasher plugins and installs their resources. + * + * Design patterns: + * - Command Pattern: Implements the command pattern for console interaction + * - Discovery Pattern: Automatically discovers and processes registered plugins + * - Template Method Pattern: Defines a structured workflow with specific steps + */ +final class InstallCommand extends Command { /** - * @return void + * Creates a new InstallCommand instance. + * + * @param AssetManagerInterface $assetManager Manager for handling PHPFlasher assets */ - protected function configure() + public function __construct(private readonly AssetManagerInterface $assetManager) + { + parent::__construct(); + } + + /** + * Configure the command options and help text. + */ + protected function configure(): void { $this ->setName('flasher:install') ->setDescription('Installs all PHPFlasher resources to the public and config directories.') - ->setHelp('The command copies PHPFlasher assets to public/vendor/flasher/ directory and config files to the config/packages/ directory without overwriting any existing config files.'); + ->setHelp('The command copies PHPFlasher assets to public/vendor/flasher/ directory and config files to the config/packages/ directory without overwriting any existing config files.') + ->addOption('config', 'c', InputOption::VALUE_NONE, 'Publish all config files to the config/packages/ directory.') + ->addOption('symlink', 's', InputOption::VALUE_NONE, 'Symlink PHPFlasher assets instead of copying them.'); } /** - * @return int + * Execute the command to install PHPFlasher resources. + * + * This method processes each registered bundle that implements PluginBundleInterface, + * installing its assets and configuration files as requested. + * + * @param InputInterface $input The input interface + * @param OutputInterface $output The output interface + * + * @return int Command status code (SUCCESS or FAILURE) */ - protected function flasherExecute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { + // Display PHPFlasher banner and info message $output->writeln(''); $output->writeln(' ██████╗ ██╗ ██╗██████╗ ███████╗██╗ █████╗ ███████╗██╗ ██╗███████╗██████╗ @@ -51,14 +82,40 @@ protected function flasherExecute(InputInterface $input, OutputInterface $output $output->writeln(' INFO Copying PHPFlasher resources...'); $output->writeln(''); + // Get application and validate it's a Symfony application + $application = $this->getApplication(); + if (!$application instanceof Application) { + return self::SUCCESS; + } + + // Process command options + $useSymlinks = (bool) $input->getOption('symlink'); + if ($useSymlinks) { + $output->writeln('Using symlinks to publish assets.'); + } else { + $output->writeln('Copying assets to the public directory.'); + } + + $publishConfig = (bool) $input->getOption('config'); + if ($publishConfig) { + $output->writeln('Publishing configuration files.'); + } + + // Prepare directories $publicDir = $this->getPublicDir().'/vendor/flasher/'; $configDir = $this->getConfigDir(); - $exitCode = 0; - /** @var KernelInterface $kernel */ - $kernel = $this->getApplication()->getKernel(); + $filesystem = new Filesystem(); + $filesystem->remove($publicDir); + $filesystem->mkdir($publicDir); + + // Process each plugin bundle + $files = []; + $exitCode = self::SUCCESS; + + $kernel = $application->getKernel(); foreach ($kernel->getBundles() as $bundle) { - if (!$bundle instanceof Bundle) { + if (!$bundle instanceof PluginBundleInterface) { continue; } @@ -66,60 +123,101 @@ protected function flasherExecute(InputInterface $input, OutputInterface $output $configFile = $bundle->getConfigurationFile(); try { - $this->publishAssets($plugin, $publicDir); - $this->publishConfig($plugin, $configDir, $configFile); + // Install assets and config + $files[] = $this->publishAssets($plugin, $publicDir, $useSymlinks); - $status = sprintf('%s', '\\' === \DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" /* HEAVY CHECK MARK (U+2714) */); - $output->writeln(sprintf(' %s %s', $status, $plugin->getAlias())); + if ($publishConfig) { + $this->publishConfig($plugin, $configDir, $configFile); + } + + // Report success + $status = \sprintf('%s', '\\' === \DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" /* HEAVY CHECK MARK (U+2714) */); + $output->writeln(\sprintf(' %s %s', $status, $plugin->getAlias())); } catch (\Exception $e) { - $exitCode = 1; - $status = sprintf('%s', '\\' === \DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */); - $output->writeln(sprintf(' %s %s %s', $status, $plugin->getAlias(), $e->getMessage())); + // Report failure + $exitCode = self::FAILURE; + $status = \sprintf('%s', '\\' === \DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */); + $output->writeln(\sprintf(' %s %s %s', $status, $plugin->getAlias(), $e->getMessage())); } } $output->writeln(''); - if (0 === $exitCode) { - $output->writeln(' SUCCESS PHPFlasher resources have been successfully installed.'); + // Display final status message + if (self::SUCCESS === $exitCode) { + $message = 'PHPFlasher resources have been successfully installed.'; + if ($publishConfig) { + $message .= ' Configuration files have been published.'; + } + if ($useSymlinks) { + $message .= ' Assets were symlinked.'; + } + $output->writeln(" SUCCESS $message"); } else { $output->writeln(' ERROR An error occurred during the installation of PHPFlasher resources.'); } + // Create asset manifest + $this->assetManager->createManifest(array_merge([], ...$files)); + $output->writeln(''); return $exitCode; } /** - * @param string|null $publicDir + * Publishes assets from the plugin's assets directory to the public directory. + * + * This method copies or symlinks asset files from the plugin's assets directory + * to the public directory for web access. + * + * @param PluginInterface $plugin The plugin containing assets + * @param string $publicDir Target directory for assets + * @param bool $useSymlinks Whether to symlink files instead of copying * - * @return void + * @return string[] List of target paths for generated manifest */ - private function publishAssets(PluginInterface $plugin, $publicDir) + private function publishAssets(PluginInterface $plugin, string $publicDir, bool $useSymlinks): array { - if (null === $publicDir) { - return; - } - $originDir = $plugin->getAssetsDir(); if (!is_dir($originDir)) { - return; + return []; } $filesystem = new Filesystem(); - $filesystem->mkdir($originDir, 0777); - $filesystem->mirror($originDir, $publicDir, Finder::create()->ignoreDotFiles(false)->in($originDir)); + $finder = new Finder(); + $finder->files()->in($originDir); + + $files = []; + + foreach ($finder as $file) { + $relativePath = trim(str_replace($originDir, '', $file->getRealPath()), \DIRECTORY_SEPARATOR); + $targetPath = $publicDir.$relativePath; + + if ($useSymlinks) { + $filesystem->symlink($file->getRealPath(), $targetPath); + } else { + $filesystem->copy($file->getRealPath(), $targetPath, true); + } + + $files[] = $targetPath; + } + + return $files; } /** - * @param string|null $configDir - * @param string $configFile + * Publishes configuration files to the application's config directory. * - * @return void + * This method copies plugin configuration files to the Symfony config directory, + * but only if the target file doesn't already exist (to avoid overwriting user customizations). + * + * @param PluginInterface $plugin The plugin containing configuration + * @param string|null $configDir Target config directory + * @param string $configFile Source configuration file path */ - private function publishConfig(PluginInterface $plugin, $configDir, $configFile) + private function publishConfig(PluginInterface $plugin, ?string $configDir, string $configFile): void { if (null === $configDir || !file_exists($configFile)) { return; @@ -135,14 +233,22 @@ private function publishConfig(PluginInterface $plugin, $configDir, $configFile) } /** - * @return string|null + * Gets the path to the public directory. + * + * This method tries to locate the public directory using multiple strategies: + * 1. First, it looks for a standard 'public' directory in the project + * 2. If not found, it falls back to the composer.json configuration + * + * @return string|null Path to the public directory or null if not found */ - private function getPublicDir() + private function getPublicDir(): ?string { $projectDir = $this->getProjectDir(); + if (null === $projectDir) { + return null; + } - $publicDir = Bridge::versionCompare('4', '>=') ? '/public' : '/web'; - $publicDir = rtrim($projectDir, '/').$publicDir; + $publicDir = rtrim($projectDir, '/').'/public'; if (is_dir($publicDir)) { return $publicDir; @@ -152,14 +258,23 @@ private function getPublicDir() } /** - * @return string|null + * Gets the path to the config directory. + * + * This method tries to locate the config/packages directory using multiple strategies: + * 1. First, it looks for a standard 'config/packages' directory in the project + * 2. If not found, it falls back to the composer.json configuration + * + * @return string|null Path to the config directory or null if not found */ - private function getConfigDir() + private function getConfigDir(): ?string { $projectDir = $this->getProjectDir(); - $configDir = Bridge::versionCompare('4', '>=') ? '/config/packages/' : '/config'; - $configDir = rtrim($projectDir, '/').$configDir; + if (null === $projectDir) { + return null; + } + + $configDir = rtrim($projectDir, '/').'/config/packages/'; if (is_dir($configDir)) { return $configDir; @@ -169,33 +284,65 @@ private function getConfigDir() } /** - * @return string + * Gets the project root directory from the kernel. + * + * @return string|null The project directory path or null if not available */ - private function getProjectDir() + private function getProjectDir(): ?string { - /** @var Container $container */ - $container = $this->getApplication()->getKernel()->getContainer(); + $kernel = $this->getKernel(); + + if (null === $kernel) { + return null; + } - return $container->hasParameter('kernel.project_dir') - ? $container->getParameter('kernel.project_dir') - : $container->getParameter('kernel.root_dir').'/../'; + $container = $kernel->getContainer(); + + $projectDir = $container->getParameter('kernel.project_dir'); + + return \is_string($projectDir) ? $projectDir : null; } /** - * @return string|null + * Gets a directory path from composer.json extra configuration. + * + * @param string $dir The directory key to look for in composer.json extra section + * + * @return string|null The directory path or null if not found */ - private function getComposerDir($dir) + private function getComposerDir(string $dir): ?string { $projectDir = $this->getProjectDir(); + if (null === $projectDir) { + return null; + } + $composerFilePath = $projectDir.'/composer.json'; if (!file_exists($composerFilePath)) { return null; } - $composerConfig = json_decode(file_get_contents($composerFilePath), true); + /** @var array{extra: array{string, string}} $composerConfig */ + $composerConfig = json_decode(file_get_contents($composerFilePath) ?: '', true); + + return $composerConfig['extra'][$dir] ?? null; + } + + /** + * Gets the kernel instance from the application. + * + * @return KernelInterface|null The Symfony kernel or null if not available + */ + private function getKernel(): ?KernelInterface + { + $application = $this->getApplication(); + + if (!$application instanceof Application) { + return null; + } - return isset($composerConfig['extra'][$dir]) ? $composerConfig['extra'][$dir] : null; + return $application->getKernel(); } } diff --git a/Component/FlasherComponent.php b/Component/FlasherComponent.php new file mode 100644 index 0000000..c3e17f8 --- /dev/null +++ b/Component/FlasherComponent.php @@ -0,0 +1,46 @@ + + * ``` + */ +final class FlasherComponent +{ + /** + * Filtering criteria for notifications. + * + * @var array + */ + public array $criteria = []; + + /** + * Presentation format (e.g., 'html', 'json'). + */ + public string $presenter = 'html'; + + /** + * Additional context for rendering. + * + * @var array + */ + public array $context = []; +} diff --git a/Container/SymfonyContainer.php b/Container/SymfonyContainer.php deleted file mode 100644 index b46048d..0000000 --- a/Container/SymfonyContainer.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - -namespace Flasher\Symfony\Container; - -use Flasher\Prime\Container\ContainerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface as BaseSymfonyContainer; - -final class SymfonyContainer implements ContainerInterface -{ - /** @var BaseSymfonyContainer */ - private $container; - - public function __construct(BaseSymfonyContainer $container) - { - $this->container = $container; - } - - /** - * {@inheritDoc} - */ - public function get($id) - { - return $this->container->get($id); - } -} diff --git a/DependencyInjection/Compiler/EventListenerCompilerPass.php b/DependencyInjection/Compiler/EventListenerCompilerPass.php new file mode 100644 index 0000000..4bcf553 --- /dev/null +++ b/DependencyInjection/Compiler/EventListenerCompilerPass.php @@ -0,0 +1,38 @@ +findDefinition('flasher.event_dispatcher'); + + foreach (array_keys($container->findTaggedServiceIds('flasher.event_listener')) as $id) { + $definition->addMethodCall('addListener', [new Reference($id)]); + } + } +} diff --git a/DependencyInjection/Compiler/EventSubscriberCompilerPass.php b/DependencyInjection/Compiler/EventSubscriberCompilerPass.php deleted file mode 100644 index 1efdfeb..0000000 --- a/DependencyInjection/Compiler/EventSubscriberCompilerPass.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ - -namespace Flasher\Symfony\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ -final class EventSubscriberCompilerPass implements CompilerPassInterface -{ - /** - * @return void - */ - public function process(ContainerBuilder $container) - { - if (!$container->has('flasher.event_dispatcher')) { - return; - } - - $definition = $container->findDefinition('flasher.event_dispatcher'); - - foreach ($container->findTaggedServiceIds('flasher.event_subscriber') as $id => $tags) { - $definition->addMethodCall('addSubscriber', array(new Reference($id))); - } - } -} diff --git a/DependencyInjection/Compiler/FactoryCompilerPass.php b/DependencyInjection/Compiler/FactoryCompilerPass.php deleted file mode 100644 index dd11d1c..0000000 --- a/DependencyInjection/Compiler/FactoryCompilerPass.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ - -namespace Flasher\Symfony\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -final class FactoryCompilerPass implements CompilerPassInterface -{ - /** - * @return void - */ - public function process(ContainerBuilder $container) - { - if (!$container->has('flasher')) { - return; - } - - $definition = $container->findDefinition('flasher'); - - foreach ($container->findTaggedServiceIds('flasher.factory') as $id => $tags) { - foreach ($tags as $attributes) { - $definition->addMethodCall('addFactory', array($attributes['alias'], new Reference($id))); - } - } - } -} diff --git a/DependencyInjection/Compiler/FlasherAwareCompilerPass.php b/DependencyInjection/Compiler/FlasherAwareCompilerPass.php deleted file mode 100644 index 278d7b8..0000000 --- a/DependencyInjection/Compiler/FlasherAwareCompilerPass.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ - -namespace Flasher\Symfony\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -final class FlasherAwareCompilerPass implements CompilerPassInterface -{ - /** - * @return void - */ - public function process(ContainerBuilder $container) - { - if (!$container->has('flasher')) { - return; - } - - $flasher = $container->findDefinition('flasher'); - - foreach ($container->findTaggedServiceIds('flasher.flasher_aware') as $id => $tags) { - $service = $container->findDefinition($id); - $service->addMethodCall('setFlasher', array($flasher)); - } - } -} diff --git a/DependencyInjection/Compiler/PresenterCompilerPass.php b/DependencyInjection/Compiler/PresenterCompilerPass.php index 915253b..31558c0 100644 --- a/DependencyInjection/Compiler/PresenterCompilerPass.php +++ b/DependencyInjection/Compiler/PresenterCompilerPass.php @@ -1,32 +1,43 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; +/** + * PresenterCompilerPass - Registers response presenters with the response manager. + * + * This compiler pass finds all services tagged with 'flasher.presenter' + * and registers them with the PHPFlasher response manager. This allows for + * automatic discovery and registration of response presenters. + * + * Design patterns: + * - Compiler Pass: Modifies container definitions during compilation + * - Service Discovery: Automatically discovers tagged services + * - Strategy Pattern: Helps set up pluggable response presentation strategies + */ final class PresenterCompilerPass implements CompilerPassInterface { /** - * @return void + * Process the container to register presenters. + * + * @param ContainerBuilder $container The service container builder */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { - if (!$container->has('flasher.response_manager')) { - return; - } - $definition = $container->findDefinition('flasher.response_manager'); foreach ($container->findTaggedServiceIds('flasher.presenter') as $id => $tags) { foreach ($tags as $attributes) { - $definition->addMethodCall('addPresenter', array($attributes['alias'], new Reference($id))); + $definition->addMethodCall('addPresenter', [ + $attributes['alias'], + new ServiceClosureArgument(new Reference($id)), + ]); } } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 1f55200..c6436eb 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1,139 +1,195 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\DependencyInjection; use Flasher\Prime\Plugin\FlasherPlugin; -use Flasher\Symfony\Bridge\DependencyInjection\FlasherConfiguration; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; -final class Configuration extends FlasherConfiguration +final readonly class Configuration implements ConfigurationInterface { - /** - * @return TreeBuilder - */ - public function getFlasherConfigTreeBuilder() + public function __construct(private FlasherPlugin $plugin) { - $plugin = new FlasherPlugin(); + } + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder($this->plugin->getName()); + $rootNode = $treeBuilder->getRootNode(); - $treeBuilder = new TreeBuilder($plugin->getName()); + $this->normalizeConfig($rootNode); - $rootNode = method_exists($treeBuilder, 'getRootNode') - ? $treeBuilder->getRootNode() - : $treeBuilder->root($plugin->getName()); // @phpstan-ignore-line + $this->addGeneralSection($rootNode); + $this->addFlashBagSection($rootNode); + $this->addPresetsSection($rootNode); + $this->addPluginsSection($rootNode); + $this->addThemesSection($rootNode); + return $treeBuilder; + } + + private function normalizeConfig(ArrayNodeDefinition $rootNode): void + { + $rootNode + ->beforeNormalization() + ->always(fn ($v): array => $this->plugin->normalizeConfig($v)) + ->end(); + } + + private function addGeneralSection(ArrayNodeDefinition $rootNode): void + { $rootNode - ->beforeNormalization() - ->always(function ($v) use ($plugin) { - return $plugin->normalizeConfig($v); - }) - ->end() ->children() ->scalarNode('default') + ->info('Default notification library (e.g., "flasher", "toastr", "noty", "notyf", "sweetalert")') + ->isRequired() ->cannotBeEmpty() - ->defaultValue($plugin->getDefault()) + ->defaultValue($this->plugin->getDefault()) + ->end() + ->scalarNode('main_script') + ->info('Path to the main PHPFlasher JavaScript file') + ->defaultValue($this->plugin->getRootScript()) + ->end() + ->booleanNode('inject_assets') + ->info('Automatically inject assets into HTML pages') + ->defaultTrue() + ->end() + ->booleanNode('translate') + ->info('Enable message translation') + ->defaultTrue() + ->end() + ->arrayNode('excluded_paths') + ->info('URL patterns to exclude from asset injection and flash_bag conversion') + ->defaultValue([ + '/^\/_profiler/', + '/^\/_fragment/', + ]) + ->scalarPrototype()->end() ->end() - ->arrayNode('root_script') - ->prototype('scalar')->end() - ->defaultValue($plugin->getRootScript()) + ->arrayNode('filter') + ->info('Criteria to filter notifications') + ->variablePrototype()->end() ->end() ->arrayNode('scripts') - ->prototype('variable')->end() + ->info('Additional JavaScript files') + ->performNoDeepMerging() + ->scalarPrototype()->end() ->end() ->arrayNode('styles') - ->prototype('variable')->end() - ->defaultValue($plugin->getStyles()) + ->info('CSS files to style notifications') + ->performNoDeepMerging() + ->scalarPrototype()->end() ->end() ->arrayNode('options') - ->prototype('scalar')->end() + ->info('Global notification options') + ->variablePrototype()->end() ->end() - ->booleanNode('use_cdn')->defaultTrue()->end() - ->booleanNode('auto_translate')->defaultTrue()->end() - ->booleanNode('auto_render')->defaultTrue()->end() - ->arrayNode('filter_criteria') - ->prototype('scalar')->end() - ->end() - ->end() - ; - - $this->addThemesSection($rootNode); - $this->addFlashBagSection($rootNode, $plugin); - $this->addPresetsSection($rootNode); + ->end(); + } - return $treeBuilder; + private function addFlashBagSection(ArrayNodeDefinition $rootNode): void + { + $rootNode + ->children() + ->variableNode('flash_bag') + ->info('Map Symfony flash messages to notification types') + ->defaultTrue() + ->end() + ->end(); } - /** - * @return void - */ - private function addThemesSection(ArrayNodeDefinition $rootNode) + private function addPresetsSection(ArrayNodeDefinition $rootNode): void { - $rootNode // @phpstan-ignore-line + $rootNode + ->fixXmlConfig('preset') ->children() - ->arrayNode('themes') - ->ignoreExtraKeys() - ->prototype('variable')->end() - ->children() - ->scalarNode('view') - ->isRequired() - ->cannotBeEmpty() + ->arrayNode('presets') + ->info('Notification presets (optional)') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('type') + ->info('Notification type (e.g., "success", "error")') + ->end() + ->scalarNode('title') + ->info('Default title') + ->end() + ->scalarNode('message') + ->info('Default message') + ->end() + ->arrayNode('options') + ->info('Additional options') + ->variablePrototype()->end() + ->end() ->end() - ->arrayNode('styles')->end() - ->arrayNode('scripts')->end() - ->arrayNode('options')->end() ->end() ->end() - ->end() - ; + ->end(); } - /** - * @return void - */ - private function addFlashBagSection(ArrayNodeDefinition $rootNode, FlasherPlugin $plugin) + private function addPluginsSection(ArrayNodeDefinition $rootNode): void { - $rootNode // @phpstan-ignore-line + $rootNode + ->fixXmlConfig('plugin') ->children() - ->arrayNode('flash_bag') - ->canBeUnset() - ->addDefaultsIfNotSet() - ->children() - ->booleanNode('enabled')->defaultTrue()->end() - ->arrayNode('mapping') - ->prototype('variable')->end() - ->defaultValue($plugin->getFlashBagMapping()) + ->arrayNode('plugins') + ->info('Additional plugins') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('view') + ->info('Custom twig view template') + ->end() + ->arrayNode('styles') + ->info('CSS files for the plugin') + ->performNoDeepMerging() + ->scalarPrototype()->end() + ->end() + ->arrayNode('scripts') + ->info('JavaScript files for the plugin') + ->performNoDeepMerging() + ->scalarPrototype()->end() + ->end() + ->arrayNode('options') + ->info('Plugin-specific options') + ->variablePrototype()->end() + ->end() ->end() ->end() ->end() - ->end() - ; + ->end(); } - /** - * @return void - */ - private function addPresetsSection(ArrayNodeDefinition $rootNode) + private function addThemesSection(ArrayNodeDefinition $rootNode): void { - $rootNode // @phpstan-ignore-line + $rootNode + ->fixXmlConfig('theme') ->children() - ->arrayNode('presets') - ->prototype('array') - ->children() - ->scalarNode('type')->end() - ->scalarNode('title')->end() - ->scalarNode('message')->end() - ->arrayNode('options') - ->useAttributeAsKey('name') - ->prototype('variable')->end() + ->arrayNode('themes') + ->info('Additional themes') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->arrayNode('styles') + ->info('CSS files for the theme') + ->performNoDeepMerging() + ->scalarPrototype()->end() + ->end() + ->arrayNode('scripts') + ->info('JavaScript files for the theme') + ->performNoDeepMerging() + ->scalarPrototype()->end() + ->end() + ->arrayNode('options') + ->info('Theme-specific options') + ->variablePrototype()->end() + ->end() ->end() ->end() ->end() - ->end() - ; + ->end(); } } diff --git a/DependencyInjection/FlasherExtension.php b/DependencyInjection/FlasherExtension.php index c212f0d..0e759fe 100644 --- a/DependencyInjection/FlasherExtension.php +++ b/DependencyInjection/FlasherExtension.php @@ -1,235 +1,172 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\DependencyInjection; -use Flasher\Prime\Config\ConfigInterface; -use Flasher\Symfony\Bridge\Bridge; -use Symfony\Component\Config\FileLocator; +use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface; +use Flasher\Prime\Plugin\FlasherPlugin; +use Flasher\Prime\Storage\Bag\ArrayBag; +use Flasher\Symfony\Attribute\AsFlasherFactory; +use Flasher\Symfony\Attribute\AsFlasherPresenter; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader; -use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; - -/** - * @phpstan-import-type ConfigType from ConfigInterface - */ -final class FlasherExtension extends Extension implements CompilerPassInterface -{ - /** - * @phpstan-param ConfigType[] $configs - * - * @return void - */ - public function load(array $configs, ContainerBuilder $container) - { - $loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('services.php'); - - /** @var ConfigType $config */ - $config = $this->processConfiguration(new Configuration(), $configs); - - $this->registerFlasherConfiguration($config, $container); - $this->registerListeners($config, $container); - $this->registerStorageManager($config, $container); - $this->registerHttpExtensions($config, $container); - $this->registerFlasherAutoConfiguration($container); - } - - /** - * @return void - */ - public function process(ContainerBuilder $container) - { - $this->registerFlasherTranslator($container); - $this->registerFlasherTemplateEngine($container); - $this->configureSessionServices($container); - } - - /** - * @phpstan-param ConfigType $config - * - * @return void - */ - private function registerFlasherConfiguration(array $config, ContainerBuilder $container) - { - $flasherConfig = $container->getDefinition('flasher.config'); - $flasherConfig->replaceArgument(0, $config); +use Symfony\Component\DependencyInjection\Extension\AbstractExtension; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - $flasher = $container->getDefinition('flasher'); - $flasher->replaceArgument(0, $config['default']); - - $presetListener = $container->getDefinition('flasher.preset_listener'); - $presetListener->replaceArgument(0, $config['presets']); - } - - /** - * @phpstan-param ConfigType $config - * - * @return void - */ - private function registerListeners(array $config, ContainerBuilder $container) +final class FlasherExtension extends AbstractExtension implements CompilerPassInterface +{ + public function __construct(private readonly FlasherPlugin $plugin) { - $this->registerSessionListener($config, $container); - $this->registerFlasherListener($config, $container); } - /** - * @return void - */ - private function registerResponseExtension(ContainerBuilder $container) + public function getAlias(): string { - $container->register('flasher.response_extension', 'Flasher\Prime\Http\ResponseExtension') - ->setPublic(false) - ->addArgument(new Reference('flasher')); + return $this->plugin->getName(); } /** - * @param array $mapping - * - * @return void + * @param array $config */ - private function registerRequestExtension(ContainerBuilder $container, array $mapping) + public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface { - $container->register('flasher.request_extension', 'Flasher\Prime\Http\RequestExtension') - ->setPublic(false) - ->addArgument(new Reference('flasher')) - ->addArgument($mapping); + return new Configuration($this->plugin); } /** - * @phpstan-param ConfigType $config - * - * @return void + * @param array{ + * default: string, + * main_script: string, + * inject_assets: bool, + * excluded_paths: list, + * presets: array, + * flash_bag: array, + * filter: array, + * plugins: array>, + * themes: array>, + * } $config */ - private function registerSessionListener(array $config, ContainerBuilder $container) + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { - if (!$config['flash_bag']['enabled']) { - return; - } + $this->registerFlasherParameters($config, $container, $builder); + $this->registerServicesForAutoconfiguration($builder); - $container->register('flasher.session_listener', 'Flasher\Symfony\EventListener\SessionListener') - ->setPublic(true) - ->addArgument(new Reference('flasher.request_extension')) - ->addTag('kernel.event_listener', array('event' => 'kernel.response')); + $container->import(__DIR__.'/../Resources/config/services.php'); } - /** - * @phpstan-param ConfigType $config - * - * @return void - */ - private function registerFlasherListener(array $config, ContainerBuilder $container) + public function process(ContainerBuilder $container): void { - if (!$config['auto_render']) { - return; - } - - $container->register('flasher.flasher_listener', 'Flasher\Symfony\EventListener\FlasherListener') - ->setPublic(true) - ->addArgument(new Reference('flasher.response_extension')) - ->addTag('kernel.event_listener', array('event' => 'kernel.response', 'priority' => -256)); + $this->registerFlasherTranslator($container); + $this->configureSessionServices($container); + $this->configureFlasherListener($container); } /** - * @phpstan-param ConfigType $config - * - * @return void + * @param array{ + * default: string, + * main_script: string, + * inject_assets: bool, + * excluded_paths: list, + * presets: array, + * flash_bag: array, + * filter: array, + * plugins: array>, + * themes: array>, + * } $config */ - private function registerStorageManager(array $config, ContainerBuilder $container) + private function registerFlasherParameters(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { - $criteria = $config['filter_criteria']; - $storageManager = $container->getDefinition('flasher.storage_manager'); - $storageManager->replaceArgument(2, $criteria); + /** @var string $projectDir */ + $projectDir = $builder->getParameter('kernel.project_dir'); + $publicDir = $projectDir.\DIRECTORY_SEPARATOR.'public'; + $assetsDir = $publicDir.\DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR.'flasher'; + $manifestPath = $assetsDir.\DIRECTORY_SEPARATOR.'manifest.json'; + + $container->parameters() + ->set('flasher', $config) + ->set('flasher.public_dir', $publicDir) + ->set('flasher.assets_dir', $assetsDir) + ->set('flasher.json_manifest_path', $manifestPath) + ->set('flasher.default', $config['default']) + ->set('flasher.main_script', $config['main_script']) + ->set('flasher.inject_assets', $config['inject_assets']) + ->set('flasher.excluded_paths', $config['excluded_paths']) + ->set('flasher.flash_bag', $config['flash_bag']) + ->set('flasher.filter', $config['filter']) + ->set('flasher.presets', $config['presets']) + ->set('flasher.plugins', $config['plugins']) + ->set('flasher.themes', $config['themes']) + ->set('flasher.resources', $this->getFlasherResources($config)); } - /** - * @return void - */ - private function registerHttpExtensions(array $config, ContainerBuilder $container) + private function registerServicesForAutoconfiguration(ContainerBuilder $builder): void { - $mapping = $config['flash_bag']['mapping']; - $this->registerRequestExtension($container, $mapping); - - $this->registerResponseExtension($container); - } + $builder->registerForAutoconfiguration(EventListenerInterface::class) + ->addTag('flasher.event_listener'); - /** - * @return void - */ - private function registerFlasherAutoConfiguration(ContainerBuilder $container) - { - if (!method_exists($container, 'registerForAutoconfiguration')) { - return; - } + $builder->registerAttributeForAutoconfiguration(AsFlasherFactory::class, static function (ChildDefinition $definition, AsFlasherFactory $attribute): void { + $definition->addTag('flasher.factory', get_object_vars($attribute)); + }); - $container - ->registerForAutoconfiguration('Flasher\Prime\Aware\FlasherAwareInterface') - ->addTag('flasher.flasher_aware'); + $builder->registerAttributeForAutoconfiguration(AsFlasherPresenter::class, static function (ChildDefinition $definition, AsFlasherPresenter $attribute): void { + $definition->addTag('flasher.presenter', get_object_vars($attribute)); + }); } - /** - * @return void - */ - private function registerFlasherTranslator(ContainerBuilder $container) + private function registerFlasherTranslator(ContainerBuilder $container): void { - $config = $container->getDefinition('flasher.config')->getArgument(0); - - $translationListener = $container->getDefinition('flasher.translation_listener'); - $translationListener->replaceArgument(1, $config['auto_translate']); // @phpstan-ignore-line - if ($container->has('translator')) { return; } $container->removeDefinition('flasher.translator'); - $translationListener->replaceArgument(0, null); } - /** - * @return void - */ - private function registerFlasherTemplateEngine(ContainerBuilder $container) + private function configureSessionServices(ContainerBuilder $container): void { - if ($container->has('twig')) { - return; + if (!$container->has('session.factory') || false === $container->getParameter('flasher.flash_bag')) { + $container->removeDefinition('flasher.session_listener'); } - $container->removeDefinition('flasher.template_engine'); - - $listener = $container->getDefinition('flasher.resource_manager'); - $listener->replaceArgument(1, null); + if (!$container->has('session.factory')) { + $container->removeDefinition('flasher.storage_bag'); + $container->register('flasher.storage_bag', ArrayBag::class); + } } - /** - * @return void - */ - private function configureSessionServices(ContainerBuilder $container) + private function configureFlasherListener(ContainerBuilder $container): void { - if ($this->isSessionEnabled($container)) { + if ($container->getParameter('flasher.inject_assets')) { return; } - $container->removeDefinition('flasher.storage_bag'); - $container->removeDefinition('flasher.session_listener'); - - $container->register('flasher.storage_bag', 'Flasher\Prime\Storage\Bag\ArrayBag'); + $container->removeDefinition('flasher.flasher_listener'); } /** - * @return bool + * Convert the Flasher configuration into a format that can be used by the ResourceManager. + * + * @param array{ + * plugins: array>, + * themes: array>, + * } $config + * + * @return array> */ - private function isSessionEnabled(ContainerBuilder $container) + private function getFlasherResources(array $config): array { - if (Bridge::versionCompare('5.3', '>=')) { - return $container->has('session.factory'); + $resources = []; + + foreach ($config['plugins'] as $name => $options) { + $resources[$name] = $options; + } + + foreach ($config['themes'] as $name => $options) { + $resources['theme.'.$name] = $options; } - return $container->has('session'); + return $resources; } } diff --git a/EventListener/FlasherListener.php b/EventListener/FlasherListener.php index ec1dedf..639d202 100644 --- a/EventListener/FlasherListener.php +++ b/EventListener/FlasherListener.php @@ -1,39 +1,66 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\EventListener; -use Flasher\Prime\Http\ResponseExtension; +use Flasher\Prime\Http\ResponseExtensionInterface; use Flasher\Symfony\Http\Request; use Flasher\Symfony\Http\Response; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; -final class FlasherListener +/** + * FlasherListener - Injects PHPFlasher assets into responses. + * + * This event subscriber listens for kernel.response events and injects + * PHPFlasher JavaScript and CSS assets into appropriate HTTP responses. + * It adapts Symfony's request and response objects to PHPFlasher's interfaces. + * + * Design patterns: + * - Observer Pattern: Observes Symfony's kernel events + * - Adapter Pattern: Adapts Symfony's request/response to PHPFlasher's interfaces + * - Event Subscriber: Subscribes to Symfony's event dispatcher system + */ +final readonly class FlasherListener implements EventSubscriberInterface { /** - * @var ResponseExtension + * Creates a new FlasherListener instance. + * + * @param ResponseExtensionInterface $responseExtension Service for extending responses with notifications */ - private $responseExtension; - - public function __construct(ResponseExtension $responseExtension) + public function __construct(private ResponseExtensionInterface $responseExtension) { - $this->responseExtension = $responseExtension; } /** - * @param ResponseEvent $event + * Processes the response to inject PHPFlasher assets. + * + * This handler adapts Symfony's request and response objects to PHPFlasher's + * interfaces, then delegates to the response extension for asset injection. * - * @return void + * @param ResponseEvent $event The response event */ - public function onKernelResponse($event) + public function onKernelResponse(ResponseEvent $event): void { $request = new Request($event->getRequest()); $response = new Response($event->getResponse()); $this->responseExtension->render($request, $response); } + + /** + * {@inheritdoc} + * + * Returns events this subscriber listens to and their corresponding handlers. + * The low priority (-20) ensures this runs after most other response listeners. + * + * @return array> The events and handlers + */ + public static function getSubscribedEvents(): array + { + return [ + ResponseEvent::class => ['onKernelResponse', -20], + ]; + } } diff --git a/EventListener/SessionListener.php b/EventListener/SessionListener.php index da0b425..040550e 100644 --- a/EventListener/SessionListener.php +++ b/EventListener/SessionListener.php @@ -1,39 +1,65 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\EventListener; -use Flasher\Prime\Http\RequestExtension; +use Flasher\Prime\Http\RequestExtensionInterface; use Flasher\Symfony\Http\Request; use Flasher\Symfony\Http\Response; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; -final class SessionListener +/** + * SessionListener - Processes session flash messages. + * + * This event subscriber listens for kernel.response events and converts + * Symfony's session flash messages to PHPFlasher notifications. This enables + * PHPFlasher to work with existing code that uses Symfony's flash messaging. + * + * Design patterns: + * - Observer Pattern: Observes Symfony's kernel events + * - Adapter Pattern: Adapts Symfony's flash messages to PHPFlasher notifications + * - Transformer Pattern: Transforms data from one format to another + */ +final readonly class SessionListener implements EventSubscriberInterface { /** - * @var RequestExtension + * Creates a new SessionListener instance. + * + * @param RequestExtensionInterface $requestExtension Service for processing request flash messages */ - private $requestExtension; - - public function __construct(RequestExtension $requestExtension) + public function __construct(private RequestExtensionInterface $requestExtension) { - $this->requestExtension = $requestExtension; } /** - * @param ResponseEvent $event + * Processes the request to convert flash messages to notifications. + * + * This handler adapts Symfony's request and response objects to PHPFlasher's + * interfaces, then delegates to the request extension for flash processing. * - * @return void + * @param ResponseEvent $event The response event */ - public function onKernelResponse($event) + public function onKernelResponse(ResponseEvent $event): void { $request = new Request($event->getRequest()); $response = new Response($event->getResponse()); $this->requestExtension->flash($request, $response); } + + /** + * {@inheritdoc} + * + * Returns events this subscriber listens to and their corresponding handlers. + * + * @return array> The events and handlers + */ + public static function getSubscribedEvents(): array + { + return [ + ResponseEvent::class => ['onKernelResponse', 0], + ]; + } } diff --git a/Factory/NotificationFactoryLocator.php b/Factory/NotificationFactoryLocator.php new file mode 100644 index 0000000..9315fe6 --- /dev/null +++ b/Factory/NotificationFactoryLocator.php @@ -0,0 +1,60 @@ + $serviceLocator Symfony's service locator + */ + public function __construct(private ServiceLocator $serviceLocator) + { + } + + /** + * {@inheritdoc} + * + * Checks if a notification factory with the given ID exists. + * + * @param string $id The factory identifier + * + * @return bool True if the factory exists, false otherwise + */ + public function has(string $id): bool + { + return $this->serviceLocator->has($id); + } + + /** + * {@inheritdoc} + * + * Gets a notification factory by ID. + * + * @param string $id The factory identifier + * + * @return NotificationFactoryInterface The notification factory + */ + public function get(string $id): NotificationFactoryInterface + { + return $this->serviceLocator->get($id); + } +} diff --git a/FlasherSymfonyBundle.php b/FlasherSymfonyBundle.php index 45c76d4..df8dd25 100644 --- a/FlasherSymfonyBundle.php +++ b/FlasherSymfonyBundle.php @@ -1,57 +1,72 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony; use Flasher\Prime\Container\FlasherContainer; use Flasher\Prime\Plugin\FlasherPlugin; -use Flasher\Symfony\Container\SymfonyContainer; -use Flasher\Symfony\DependencyInjection\Compiler\EventSubscriberCompilerPass; -use Flasher\Symfony\DependencyInjection\Compiler\FactoryCompilerPass; -use Flasher\Symfony\DependencyInjection\Compiler\FlasherAwareCompilerPass; +use Flasher\Symfony\DependencyInjection\Compiler\EventListenerCompilerPass; use Flasher\Symfony\DependencyInjection\Compiler\PresenterCompilerPass; use Flasher\Symfony\DependencyInjection\FlasherExtension; -use Flasher\Symfony\Support\Bundle; +use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; -class FlasherSymfonyBundle extends Bundle // Symfony\Component\HttpKernel\Bundle\Bundle +/** + * FlasherSymfonyBundle - Main bundle for PHPFlasher Symfony integration. + * + * This bundle serves as the entry point for integrating PHPFlasher with Symfony. + * It registers compiler passes, configures the container extension, and sets up + * the global container instance for PHPFlasher. + * + * Design patterns: + * - Bundle: Implements Symfony's bundle pattern for packaging functionality + * - Registry: Sets up the container registry for PHPFlasher + * - Extension: Extends the base plugin bundle with PHPFlasher-specific functionality + */ +final class FlasherSymfonyBundle extends Support\PluginBundle // Symfony\Component\HttpKernel\Bundle\Bundle { /** - * {@inheritdoc} + * Set up the global container reference when the bundle boots. + * + * This allows PHPFlasher to access services from Symfony's container. */ - public function boot() + public function boot(): void { - FlasherContainer::init(new SymfonyContainer($this->container)); + if ($this->container instanceof ContainerInterface) { + FlasherContainer::from($this->container); + } } /** - * {@inheritDoc} + * Register compiler passes with the container. + * + * @param ContainerBuilder $container The container builder */ - public function createPlugin() + public function build(ContainerBuilder $container): void { - return new FlasherPlugin(); + $container->addCompilerPass(new EventListenerCompilerPass()); + $container->addCompilerPass(new PresenterCompilerPass()); } /** - * {@inheritdoc} + * Get the container extension for this bundle. + * + * @return ExtensionInterface The bundle extension */ - protected function flasherBuild(ContainerBuilder $container) + public function getContainerExtension(): ExtensionInterface { - $container->addCompilerPass(new FactoryCompilerPass()); - $container->addCompilerPass(new EventSubscriberCompilerPass()); - $container->addCompilerPass(new PresenterCompilerPass()); - $container->addCompilerPass(new FlasherAwareCompilerPass()); + return new FlasherExtension($this->createPlugin()); } /** - * {@inheritdoc} + * Create the core PHPFlasher plugin. + * + * @return FlasherPlugin The core PHPFlasher plugin */ - protected function getFlasherContainerExtension() + public function createPlugin(): FlasherPlugin { - return new FlasherExtension(); + return new FlasherPlugin(); } } diff --git a/Http/Request.php b/Http/Request.php index 5b7af35..5d28d53 100644 --- a/Http/Request.php +++ b/Http/Request.php @@ -1,90 +1,118 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\Http; use Flasher\Prime\Http\RequestInterface; +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; -use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; -final class Request implements RequestInterface +/** + * Request - Adapter for Symfony's HTTP request. + * + * This class implements PHPFlasher's RequestInterface for Symfony's HTTP request, + * providing a consistent interface for request inspection and session interaction + * regardless of the underlying framework. + * + * Design patterns: + * - Adapter Pattern: Adapts framework-specific request to PHPFlasher's interface + * - Decorator Pattern: Adds PHPFlasher-specific functionality to request objects + * - Null Object Pattern: Gracefully handles missing sessions + */ +final readonly class Request implements RequestInterface { /** - * @var SymfonyRequest + * Creates a new Request adapter. + * + * @param SymfonyRequest $request The underlying Symfony request object */ - private $request; + public function __construct(private SymfonyRequest $request) + { + } - public function __construct(SymfonyRequest $request) + public function getUri(): string { - $this->request = $request; + return $this->request->getRequestUri(); } - /** - * {@inheritDoc} - */ - public function isXmlHttpRequest() + public function isXmlHttpRequest(): bool { return $this->request->isXmlHttpRequest(); } - /** - * {@inheritDoc} - */ - public function isHtmlRequestFormat() + public function isHtmlRequestFormat(): bool { return 'html' === $this->request->getRequestFormat(); } - /** - * {@inheritDoc} - */ - public function hasSession() + public function hasSession(): bool { return $this->request->hasSession(); } - /** - * {@inheritDoc} - */ - public function hasType($type) + public function isSessionStarted(): bool + { + $session = $this->getSession(); + + return $session?->isStarted() ?: false; + } + + public function hasType(string $type): bool { - if (!$this->hasSession()) { + if (!$this->hasSession() || !$this->isSessionStarted()) { return false; } - $session = $this->request->getSession(); - if (!$session->isStarted()) { + $session = $this->getSession(); + if (!$session instanceof FlashBagAwareSessionInterface) { return false; } - /** @var Session $session */ - $session = $this->request->getSession(); - $flashBag = $session->getFlashBag(); - - return $flashBag->has($type); + return $session->getFlashBag()->has($type); } /** - * {@inheritDoc} + * @return string[] */ - public function getType($type) + public function getType(string $type): array { - /** @var Session $session */ - $session = $this->request->getSession(); - $flashBag = $session->getFlashBag(); + $session = $this->getSession(); + if (!$session instanceof FlashBagAwareSessionInterface) { + return []; + } + + return $session->getFlashBag()->get($type); + } - return $flashBag->get($type); + public function forgetType(string $type): void + { + $this->getType($type); } /** - * {@inheritDoc} + * Gets the session from the request, with graceful handling of missing sessions. + * + * @return SessionInterface|null The session or null if not available */ - public function forgetType($type) + private function getSession(): ?SessionInterface { - $this->getType($type); + try { + return $this->request->getSession(); + } catch (SessionNotFoundException) { + return null; + } + } + + public function hasHeader(string $key): bool + { + return $this->request->headers->has($key); + } + + public function getHeader(string $key): ?string + { + return $this->request->headers->get($key); } } diff --git a/Http/Response.php b/Http/Response.php index 4ba33b2..66097d4 100644 --- a/Http/Response.php +++ b/Http/Response.php @@ -1,9 +1,6 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\Http; @@ -11,38 +8,40 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -final class Response implements ResponseInterface +/** + * Response - Adapter for Symfony's HTTP response. + * + * This class implements PHPFlasher's ResponseInterface for Symfony's HTTP response, + * providing a consistent interface for response manipulation regardless of the framework. + * It allows PHPFlasher to work with Symfony responses in a framework-agnostic way. + * + * Design patterns: + * - Adapter Pattern: Adapts framework-specific response to PHPFlasher's interface + * - Decorator Pattern: Adds PHPFlasher-specific functionality to response objects + * - Composition: Uses composition to delegate to the underlying response object + */ +final readonly class Response implements ResponseInterface { /** - * @var SymfonyResponse + * Creates a new Response adapter. + * + * @param SymfonyResponse $response The underlying Symfony response object */ - private $response; - - public function __construct(SymfonyResponse $response) + public function __construct(private SymfonyResponse $response) { - $this->response = $response; } - /** - * {@inheritDoc} - */ - public function isRedirection() + public function isRedirection(): bool { return $this->response->isRedirection(); } - /** - * {@inheritDoc} - */ - public function isJson() + public function isJson(): bool { return $this->response instanceof JsonResponse; } - /** - * {@inheritDoc} - */ - public function isHtml() + public function isHtml(): bool { $contentType = $this->response->headers->get('Content-Type'); @@ -53,35 +52,49 @@ public function isHtml() return false !== stripos($contentType, 'html'); } - /** - * {@inheritDoc} - */ - public function isAttachment() + public function isAttachment(): bool { $contentDisposition = $this->response->headers->get('Content-Disposition', ''); - if (!\is_string($contentDisposition)) { + if (!$contentDisposition) { return false; } return false !== stripos($contentDisposition, 'attachment;'); } - /** - * {@inheritDoc} - */ - public function getContent() + public function isSuccessful(): bool { - $content = $this->response->getContent(); + return $this->response->isSuccessful(); + } - return \is_string($content) ? $content : ''; + public function getContent(): string + { + return $this->response->getContent() ?: ''; } - /** - * {@inheritDoc} - */ - public function setContent($content) + public function setContent(string $content): void { $this->response->setContent($content); } + + public function hasHeader(string $key): bool + { + return $this->response->headers->has($key); + } + + public function getHeader(string $key): ?string + { + return $this->response->headers->get($key); + } + + public function setHeader(string $key, array|string|null $values): void + { + $this->response->headers->set($key, $values); + } + + public function removeHeader(string $key): void + { + $this->response->headers->remove($key); + } } diff --git a/LICENSE b/LICENSE index 8e94bc1..cf3a76d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 PHPFlasher +Copyright (c) 2024 PHPFlasher Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Profiler/FlasherDataCollector.php b/Profiler/FlasherDataCollector.php new file mode 100644 index 0000000..2acd4a0 --- /dev/null +++ b/Profiler/FlasherDataCollector.php @@ -0,0 +1,191 @@ +, + * metadata: array, + * } + * @phpstan-type ConfigShare array{ + * default: string, + * main_script: string, + * inject_assets: bool, + * excluded_paths: list, + * presets: array, + * flash_bag: array, + * filter: array{limit?: int|null}, + * plugins: array>, + * } + * @phpstan-type DataShape array{ + * displayed_envelopes: NotificationShape[], + * dispatched_envelopes: NotificationShape[], + * config: array, + * versions: array{ + * php_flasher: string, + * php: string, + * symfony: string + * } + * } + * + * @property DataShape|Data $data + * + * @internal + */ +#[\AllowDynamicProperties] +final class FlasherDataCollector extends AbstractDataCollector implements LateDataCollectorInterface +{ + /** + * Creates a new FlasherDataCollector instance. + * + * @param NotificationLoggerListener $logger The notification logger for accessing dispatched notifications + * @param array $config The PHPFlasher configuration + * + * @phpstan-param ConfigShare $config + */ + public function __construct( + private readonly NotificationLoggerListener $logger, + private readonly array $config, + ) { + } + + /** + * Initial data collection - called during request processing. + * + * This implementation doesn't collect data here, deferring to lateCollect. + * + * @param Request $request The request object + * @param Response $response The response object + * @param \Throwable|null $exception Any exception that occurred + */ + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + // No action needed here since we're collecting data in lateCollect + } + + /** + * Late data collection - called after response is sent. + * + * Collects information about notifications, configuration, and versions. + */ + public function lateCollect(): void + { + $this->data = [ + 'displayed_envelopes' => array_map(fn (Envelope $envelope) => $envelope->toArray(), $this->logger->getDisplayedEnvelopes()->getEnvelopes()), + 'dispatched_envelopes' => array_map(fn (Envelope $envelope) => $envelope->toArray(), $this->logger->getDispatchedEnvelopes()->getEnvelopes()), + 'config' => $this->config, + 'versions' => [ + 'php_flasher' => Flasher::VERSION, + 'php' => \PHP_VERSION, + 'symfony' => Kernel::VERSION, + ], + ]; + + $this->data = $this->cloneVar($this->data); + } + + /** + * Gets the collector data. + * + * @return DataShape|Data + */ + public function getData(): array|Data + { + return $this->data; + } + + /** + * Gets the collector name for the profiler panel. + * + * @return string The collector name + */ + public function getName(): string + { + return 'flasher'; + } + + /** + * Resets the collector between requests when using kernel.reset. + */ + public function reset(): void + { + $this->logger->reset(); + parent::reset(); + } + + /** + * Gets the displayed notification envelopes. + * + * @return NotificationShape[]|Data + */ + public function getDisplayedEnvelopes(): array|Data + { + return $this->data['displayed_envelopes'] ?? []; + } + + /** + * Gets the dispatched notification envelopes. + * + * @return NotificationShape[]|Data + */ + public function getDispatchedEnvelopes(): array|Data + { + return $this->data['dispatched_envelopes'] ?? []; + } + + /** + * Gets the PHPFlasher configuration. + * + * @phpstan-return ConfigShare|Data + */ + public function getConfig(): array|Data + { + return $this->data['config'] ?? []; + } + + /** + * Gets version information. + * + * @return array{php_flasher: string, php: string, symfony: string}|Data + */ + public function getVersions(): array|Data + { + return $this->data['versions'] ?? []; + } + + /** + * Gets the template path for the profiler panel. + * + * @return string The template path + */ + public static function getTemplate(): string + { + return '@FlasherSymfony/profiler/flasher.html.twig'; + } +} diff --git a/README.md b/README.md index 70a05eb..c613a52 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,345 @@ + +

- - PHPFlasher Logo + + PHPFlasher Logo

-## About PHPFlasher +

+ Author Badge + Source Code Badge + GitHub Release Badge + License Badge + Packagist Downloads Badge + GitHub Stars Badge + Supported PHP Version Badge +

+ +## Table of Contents + +- [About PHPFlasher Symfony Adapter](#about-phpflasher-symfony-adapter) +- [Features](#features) +- [Supported Versions](#supported-versions) +- [Installation](#installation) + - [Core Package](#core-package) + - [Adapters](#adapters) +- [Configuration](#configuration) + - [Configuration File](#configuration-file) + - [Configuration Options](#configuration-options) +- [Quick Start](#quick-start) +- [Usage Examples](#usage-examples) +- [Adapters Overview](#adapters-overview) +- [Official Documentation](#official-documentation) +- [Contributors and Sponsors](#contributors-and-sponsors) +- [Contact](#contact) +- [License](#license) + +## About PHPFlasher Symfony Adapter + +**PHPFlasher Symfony Adapter** is an open-source package that seamlessly integrates PHPFlasher’s robust flash messaging capabilities into your **Symfony** applications. It streamlines the process of adding flash messages, offering an intuitive API to enhance user experience with minimal configuration. + +With PHPFlasher Symfony Adapter, you can effortlessly display success, error, warning, and informational messages to your users, ensuring clear communication of application states and actions. + +## Features + +- **Seamless Symfony Integration**: Designed specifically for Symfony, ensuring compatibility and ease of use. +- **Multiple Notification Libraries**: Supports various frontend libraries like Toastr, Noty, SweetAlert, and Notyf. +- **Flexible Configuration**: Customize the appearance and behavior of flash messages to fit your application's needs. +- **Intuitive API**: Simple methods to create and manage flash messages without boilerplate code. +- **Extensible**: Easily add or create new adapters for different frontend libraries. + +## Supported Versions + +| PHPFlasher Symfony Adapter Version | PHP Version | Symfony Version | +|------------------------------------|-------------|-----------------| +| **v2.x** | ≥ 8.2 | ≥ 7.0 | +| **v1.x** | ≥ 5.3 | ≥ 2.0 | + +> **Note:** Ensure your project meets the PHP and Symfony version requirements for the PHPFlasher Symfony Adapter version you intend to use. For older PHP or Symfony versions, refer to [PHPFlasher v1.x](https://github.com/php-flasher/flasher-symfony/tree/1.x). + +## Installation + +### Core Package + +Install the PHPFlasher Symfony Adapter via Composer: + +```bash +composer require php-flasher/flasher-symfony +``` + +After installation, set up the necessary assets: + +```shell +php bin/console flasher:install +``` + +> **Note:** PHPFlasher automatically injects the necessary JavaScript and CSS assets into your Blade templates. No additional steps are required for asset injection. + +### Adapters + +PHPFlasher provides various adapters for different notification libraries. Below is an overview of available adapters for Symfony: + +- [flasher-toastr-symfony](https://github.com/php-flasher/flasher-toastr-symfony) - Symfony Adapter +- [flasher-noty-symfony](https://github.com/php-flasher/flasher-noty-symfony) - Symfony Adapter +- [flasher-notyf-symfony](https://github.com/php-flasher/flasher-notyf-symfony) - Symfony Adapter +- [flasher-sweetalert-symfony](https://github.com/php-flasher/flasher-sweetalert-symfony) - Symfony Adapter + +For detailed installation and usage instructions for each adapter, refer to their respective `README.md`. + +## Configuration + +After installing the PHPFlasher Symfony Adapter, you can configure it by publishing the configuration file or by modifying it directly. + +### Configuration File + +If you need to customize the default settings, publish the configuration file using the following command: + +```bash +php bin/console flasher:install --config +``` + +This will create a file at `config/packages/flasher.yaml` with the following content: + +```yaml +flasher: + # Default notification library (e.g., 'flasher', 'toastr', 'noty', 'notyf', 'sweetalert') + default: flasher + + # Path to the main PHPFlasher JavaScript file + main_script: '/vendor/flasher/flasher.min.js' + + # List of CSS files to style your notifications + styles: + - '/vendor/flasher/flasher.min.css' + + # Set global options for all notifications (optional) + # options: + # # Time in milliseconds before the notification disappears + # timeout: 5000 + # # Where the notification appears on the screen + # position: 'top-right' + + # Automatically inject JavaScript and CSS assets into your HTML pages + inject_assets: true + + # Enable message translation using Symfony's translation service + translate: true + + # URL patterns to exclude from asset injection and flash_bag conversion + excluded_paths: + - '/^\/_profiler/' + - '/^\/_fragment/' + + # Map Symfony flash message keys to notification types + flash_bag: + success: ['success'] + error: ['error', 'danger'] + warning: ['warning', 'alarm'] + info: ['info', 'notice', 'alert'] + + # Set criteria to filter which notifications are displayed (optional) + # filter: + # # Maximum number of notifications to show at once + # limit: 5 + + # Define notification presets to simplify notification creation (optional) + # presets: + # # Example preset: + # entity_saved: + # type: 'success' + # title: 'Entity saved' + # message: 'Entity saved successfully' +``` + +### Configuration Options + +| **Option** | **Description** | +|------------------|---------------------------------------------------------------------------------------------------------------------------| +| `default` | **String**: The default notification library to use (e.g., `'flasher'`, `'toastr'`, `'noty'`, `'notyf'`, `'sweetalert'`). | +| `main_script` | **String**: Path to the main PHPFlasher JavaScript file. | +| `styles` | **Array**: List of CSS files to style your notifications. | +| `options` | **Array** (Optional): Global options for all notifications (e.g., `'timeout'`, `'position'`). | +| `inject_assets` | **Boolean**: Whether to automatically inject JavaScript and CSS assets into your HTML pages. | +| `translate` | **Boolean**: Enable message translation using Symfony’s translation service. | +| `excluded_paths` | **Array**: URL patterns to exclude from asset injection and flash_bag conversion. | +| `flash_bag` | **Array**: Map Symfony flash message keys to notification types. | +| `filter` | **Array** (Optional): Criteria to filter which notifications are displayed (e.g., `'limit'`). | +| `presets` | **Array** (Optional): Define notification presets to simplify notification creation. | + +## Quick Start + +To display a notification message, you can either use the `flash()` helper function or obtain an instance of `flasher` from the service container. Then, before returning a view or redirecting, call the desired method (`success()`, `error()`, etc.) and pass in the message to be displayed. + +### Using the `flash()` Helper + +```php +redirectToRoute('book_list'); + } +} +``` + +### Using the `flasher` Service + +```php +flasher = $flasher; + } + + public function register(): RedirectResponse + { + // Your logic here + + $this->flasher->success('Your changes have been saved!'); + + // ... redirect or render the view + return $this->redirectToRoute('home'); + } + + public function update(): RedirectResponse + { + // Your logic here + + $this->flasher->error('An error occurred while updating.'); + + return $this->redirectToRoute('update_page'); + } +} +``` + +## Usage Examples + +### Success Message + +```php +flash()->success('Operation completed successfully!'); +``` + +### Error Message + +```php +flash()->error('An error occurred.'); +``` + +### Info Message + +```php +flash()->info('This is an informational message.'); +``` + +### Warning Message + +```php +flash()->warning('This is a warning message.'); +``` + +### Passing Options + +```php +flash()->success('Custom message with options.', ['timeout' => 3000, 'position' => 'bottom-left']); +``` + +### Using presets + +Define a preset in your `config/packages/flasher.yaml`: + +```yaml +flasher: + # ... other configurations + + presets: + entity_saved: + type: 'success' + title: 'Entity Saved' + message: 'The entity has been saved successfully.' + entity_deleted: + type: 'warning' + title: 'Entity Deleted' + message: 'The entity has been deleted.' +``` + +Use the preset in your controller: + +```php +preset('entity_saved'); + + return $this->redirectToRoute('books.index'); + } + + public function delete(): RedirectResponse + { + // Your deletion logic + + flash()->preset('entity_deleted'); + + return $this->redirectToRoute('books.index'); + } +} +``` -PHPFlasher is a powerful and easy-to-use package that allows you to quickly and easily add flash messages to your Laravel or Symfony projects. -Whether you need to alert users of a successful form submission, an error, or any other important information, flash messages are a simple and effective solution for providing feedback to your users. +## Adapters Overview -With PHPFlasher, you can easily record and store messages within the session, making it simple to retrieve and display them on the current or next page. -This improves user engagement and enhances the overall user experience on your website or application. +PHPFlasher supports various adapters to integrate seamlessly with different frontend libraries. Below is an overview of available adapters for Symfony: -Whether you're a beginner or an experienced developer, PHPFlasher's intuitive and straightforward design makes it easy to integrate into your projects. -So, if you're looking for a reliable, flexible and easy to use flash messages solution, PHPFlasher is the perfect choice. +| Adapter Repository | Description | +|-----------------------------------------------------------------------------------------|--------------------------------| +| [flasher-symfony](https://github.com/php-flasher/flasher-symfony) | Symfony framework adapter | +| [flasher-toastr-symfony](https://github.com/php-flasher/flasher-toastr-symfony) | Toastr adapter for Symfony | +| [flasher-noty-symfony](https://github.com/php-flasher/flasher-noty-symfony) | Noty adapter for Symfony | +| [flasher-notyf-symfony](https://github.com/php-flasher/flasher-notyf-symfony) | Notyf adapter for Symfony | +| [flasher-sweetalert-symfony](https://github.com/php-flasher/flasher-sweetalert-symfony) | SweetAlert adapter for Symfony | +> **Note:** Each adapter has its own repository. For detailed installation and usage instructions, please refer to the [Official Documentation](https://php-flasher.io). ## Official Documentation -Documentation for PHPFlasher can be found on the [https://php-flasher.io](https://php-flasher.io). +Comprehensive documentation for PHPFlasher is available at [https://php-flasher.io](https://php-flasher.io). Here you will find detailed guides, API references, and advanced usage examples to help you get the most out of PHPFlasher. ## Contributors and sponsors @@ -36,7 +356,7 @@ Shining stars of our community: - + @@ -46,7 +366,7 @@ Shining stars of our community: - +
Younes KHOUBZA
Younes KHOUBZA

💻 📖 🚧
Younes ENNAJI
Younes ENNAJI

💻 📖 🚧
Salma Mourad
Salma Mourad

💵
Nashwan Abdullah
Nashwan Abdullah

💵
Arvid de Jong
Arvid de Jong

💵
Lucas Maciel
Lucas Maciel

🎨
Antoni Siek
Antoni Siek

💻
Ahmed Gamal
Ahmed Gamal

💻 📖
@@ -58,17 +378,17 @@ Shining stars of our community: ## Contact -PHPFlasher is being actively developed by yoeunes. +PHPFlasher is being actively developed by yoeunes. You can reach out with questions, bug reports, or feature requests on any of the following: -- [Github Issues](https://github.com/php-flasher/php-flasher/issues) +- [Github Issues](https://github.com/php-flasher/php-flasher/issues) - [Github](https://github.com/yoeunes) - [Twitter](https://twitter.com/yoeunes) -- [Linkedin](https://www.linkedin.com/in/younes-khoubza/) -- [Email me directly](mailto:younes.khoubza@gmail.com) +- [Linkedin](https://www.linkedin.com/in/younes--ennaji/) +- [Email me directly](mailto:younes.ennaji.pro@gmail.com) ## License PHPFlasher is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). -

Made with ❤️ by Younes KHOUBZA

+

Made with ❤️ by Younes ENNAJI

diff --git a/Resources/config/config.yaml b/Resources/config/config.yaml index b0d1f93..1f472bc 100644 --- a/Resources/config/config.yaml +++ b/Resources/config/config.yaml @@ -1,173 +1,48 @@ flasher: - # -------------------------------------------------------------------------- - # Default PHPFlasher library - # -------------------------------------------------------------------------- - # This option controls the default library that will be used by PHPFlasher - # to display notifications in your Symfony application. PHPFlasher supports - # several libraries, including "flasher", "toastr", "noty", "notyf", - # "sweetalert" and "pnotify". - # - # The "flasher" library is used by default. If you want to use a different - # library, you will need to install it using composer. For example, to use - # the "toastr" library, run the following command: - # composer require php-flasher/flasher-toastr-symfony - # - # Here is a list of the supported libraries and the corresponding composer - # commands to install them: - # - # "toastr" : composer require php-flasher/flasher-toastr-symfony - # "noty" : composer require php-flasher/flasher-noty-symfony - # "notyf" : composer require php-flasher/flasher-notyf-symfony - # "sweetalert" : composer require php-flasher/flasher-sweetalert-symfony - # "pnotify" : composer require php-flasher/flasher-pnotify-symfony - # + # Default notification library (e.g., 'flasher', 'toastr', 'noty', 'notyf', 'sweetalert') default: flasher - # -------------------------------------------------------------------------- - # Main PHPFlasher javascript file - # -------------------------------------------------------------------------- - # This option specifies the location of the main javascript file that is - # required by PHPFlasher to display notifications in your Symfony application. - # - # By default, PHPFlasher uses a CDN to serve the latest version of the library. - # However, you can also choose to download the library locally or install it - # using npm. - # - # To use the local version of the library, run the following command: - # php bin/console flasher:install - # - # This will copy the necessary assets to your application's public folder. - # You can then specify the local path to the javascript file in the 'local' - # field of this option. - # - root_script: - cdn: 'https://cdn.jsdelivr.net/npm/@flasher/flasher@1.3.2/dist/flasher.min.js' - local: '/vendor/flasher/flasher.min.js' + # Path to the main PHPFlasher JavaScript file + main_script: '/vendor/flasher/flasher.min.js' - # -------------------------------------------------------------------------- - # PHPFlasher Stylesheet - # -------------------------------------------------------------------------- - # This option specifies the location of the stylesheet file that is - # required by PHPFlasher to style the notifications in your Symfony application. - # - # By default, PHPFlasher uses a CDN to serve the latest version of the stylesheet. - # However, you can also choose to download the stylesheet locally or include it - # from your assets. - # - # To use the local version of the stylesheet, make sure you have the necessary - # assets in your application's public folder. Then specify the local path to - # the stylesheet file in the 'local' field of this option. - # + # List of CSS files to style your notifications styles: - cdn: 'https://cdn.jsdelivr.net/npm/@flasher/flasher@1.3.2/dist/flasher.min.css' - local: '/vendor/flasher/flasher.min.css' + - '/vendor/flasher/flasher.min.css' - # -------------------------------------------------------------------------- - # Whether to use CDN for PHPFlasher assets or not - # -------------------------------------------------------------------------- - # This option controls whether PHPFlasher should use CDN links or local assets - # for its javascript and CSS files. By default, PHPFlasher uses CDN links - # to serve the latest version of the library. However, you can also choose - # to use local assets by setting this option to 'false'. - # - # If you decide to use local assets, don't forget to publish the necessary - # files to your application's public folder by running the following command: - # php bin/console flasher:install - # - # This will copy the necessary assets to your application's public folder. - # - use_cdn: true + # Set global options for all notifications (optional) + # options: + # # Time in milliseconds before the notification disappears + # timeout: 5000 + # # Where the notification appears on the screen + # position: 'top-right' - # -------------------------------------------------------------------------- - # Translate PHPFlasher messages - # -------------------------------------------------------------------------- - # This option controls whether PHPFlasher should pass its messages to the Symfony's - # translation service for localization. - # - # By default, this option is set to 'true', which means that PHPFlasher will - # attempt to translate its messages using the translation service. - # - # If you don't want PHPFlasher to use the Symfony's translation service, you can - # set this option to 'false'. In this case, PHPFlasher will use the messages - # as-is, without attempting to translate them. - # - auto_translate: true + # Automatically inject JavaScript and CSS assets into your HTML pages + inject_assets: true - # -------------------------------------------------------------------------- - # Inject PHPFlasher in Response - # -------------------------------------------------------------------------- - # This option controls whether PHPFlasher should automatically inject its - # javascript and CSS files into the HTML response of your Symfony application. - # - # By default, this option is set to 'true', which means that PHPFlasher will - # listen to the response of your application and automatically insert its - # scripts and stylesheets into the HTML before the closing `` tag. - # - # If you don't want PHPFlasher to automatically inject its scripts and stylesheets - # into the response, you can set this option to 'false'. In this case, you will - # need to manually include the necessary files in your application's layout. - # - auto_render: true + # Enable message translation using Symfony's translation service + translate: true - flash_bag: - # ----------------------------------------------------------------------- - # Enable flash bag - # ----------------------------------------------------------------------- - # This option controls whether PHPFlasher should automatically convert - # Symfony's flash messages to PHPFlasher notifications. This feature is - # useful when you want to migrate from a legacy system or another - # library that uses similar conventions for flash messages. - # - # When this option is set to 'true', PHPFlasher will check for flash - # messages in the session and convert them to notifications using the - # mapping specified in the 'mapping' option. When this option is set - # to 'false', PHPFlasher will ignore flash messages in the session. - # - enabled: true - - - # ----------------------------------------------------------------------- - # Flash bag type mapping - # ----------------------------------------------------------------------- - # This option allows you to map or convert session keys to PHPFlasher - # notification types. On the left side are the PHPFlasher types. - # On the right side are the Symfony session keys that you want to - # convert to PHPFlasher types. - # - # For example, if you want to convert Symfony's 'danger' flash - # messages to PHPFlasher's 'error' notifications, you can add - # the following entry to the mapping: - # error: ['danger'], - # - mapping: - success: ['success'] - error: ['error', 'danger'] - warning: ['warning', 'alarm'] - info: ['info', 'notice', 'alert'] + # URL patterns to exclude from asset injection and flash_bag conversion + excluded_paths: + - '/^\/_profiler/' + - '/^\/_fragment/' - - # ----------------------------------------------------------------------- - # Global Filter Criteria - # ----------------------------------------------------------------------- - # This option allows you to filter the notifications that are displayed - # in your Symfony application. By default, all notifications are displayed, - # but you can use this option to limit the number of notifications or - # filter them by type. - # - # For example, to limit the number of notifications to 5, you can set - # the 'limit' field to 5: + # Map Symfony flash message keys to notification types + flash_bag: + success: ['success'] + error: ['error', 'danger'] + warning: ['warning', 'alarm'] + info: ['info', 'notice', 'alert'] + + # Set criteria to filter which notifications are displayed (optional) + # filter: + # # Maximum number of notifications to show at once # limit: 5 - # - # To filter the notifications by type, you can specify an array of - # types that you want to display. For example, to only display - # error notifications, you can set the 'types' field to ['error']: - # types: ['error'], - # - # You can also combine multiple criteria by specifying multiple fields. - # For example, to display up to 5 error notifications, you can set - # the 'limit' and 'types' fields like this: - # limit: 5, - # types: ['error'], - # - filter_criteria: - limit: 5 + + # Define notification presets to simplify notification creation (optional) + # presets: + # # Example preset: + # entity_saved: + # type: 'success' + # title: 'Entity saved' + # message: 'Entity saved successfully' diff --git a/Resources/config/services.php b/Resources/config/services.php index f228c7b..d458a9c 100644 --- a/Resources/config/services.php +++ b/Resources/config/services.php @@ -1,90 +1,159 @@ - */ - -use Flasher\Symfony\Bridge\Bridge; -use Symfony\Component\DependencyInjection\Reference; - -if (!isset($container)) { - return; -} - -$container->register('flasher.config', 'Flasher\Prime\Config\Config') - ->setPublic(false) - ->addArgument(array()); - -$storage = Bridge::versionCompare('5.3', '>=') - ? new Reference('request_stack') - : new Reference('session'); - -$container->register('flasher.storage_bag', 'Flasher\Symfony\Storage\SessionBag') - ->setPublic(false) - ->addArgument($storage); - -$container->register('flasher.storage', 'Flasher\Prime\Storage\StorageBag') - ->setPublic(false) - ->addArgument(new Reference('flasher.storage_bag')); - -$container->register('flasher.event_dispatcher', 'Flasher\Prime\EventDispatcher\EventDispatcher') - ->setPublic(false); - -$container->register('flasher.storage_manager', 'Flasher\Prime\Storage\StorageManager') - ->setPublic(false) - ->addArgument(new Reference('flasher.storage')) - ->addArgument(new Reference('flasher.event_dispatcher')) - ->addArgument(array()); - -$container->register('flasher.twig.extension', 'Flasher\Symfony\Twig\FlasherTwigExtension') - ->setPublic(false) - ->addTag('twig.extension', array()); - -$container->register('flasher.template_engine', 'Flasher\Symfony\Template\TwigTemplateEngine') - ->setPublic(false) - ->addArgument(new Reference('twig')); - -$container->register('flasher.resource_manager', 'Flasher\Prime\Response\Resource\ResourceManager') - ->setPublic(false) - ->addArgument(new Reference('flasher.config')) - ->addArgument(new Reference('flasher.template_engine')); - -$container->register('flasher.response_manager', 'Flasher\Prime\Response\ResponseManager') - ->setPublic(false) - ->addArgument(new Reference('flasher.resource_manager')) - ->addArgument(new Reference('flasher.storage_manager')) - ->addArgument(new Reference('flasher.event_dispatcher')); - -$container->register('flasher', 'Flasher\Prime\Flasher') - ->setPublic(true) - ->addArgument(null) - ->addArgument(new Reference('flasher.response_manager')) - ->addArgument(new Reference('flasher.storage_manager')); - -$container->register('flasher.notification_factory', 'Flasher\Prime\Factory\NotificationFactory') - ->setPublic(false) - ->addArgument(new Reference('flasher.storage_manager')); - -$container->register('flasher.translator', 'Flasher\Symfony\Translation\Translator') - ->setPublic(false) - ->addArgument(new Reference('translator')); - -$container->register('flasher.translation_listener', 'Flasher\Prime\EventDispatcher\EventListener\TranslationListener') - ->setPublic(false) - ->addArgument(new Reference('flasher.translator')) - ->addArgument(true) - ->addTag('flasher.event_subscriber'); - -$container->register('flasher.preset_listener', 'Flasher\Prime\EventDispatcher\EventListener\PresetListener') - ->setPublic(false) - ->addArgument(array()) - ->addTag('flasher.event_subscriber'); - -$container->register('flasher.install_command', 'Flasher\Symfony\Command\InstallCommand') - ->addTag('console.command'); - -if (Bridge::canLoadAliases()) { - $container->setAlias('Flasher\Prime\Flasher', 'flasher'); - $container->setAlias('Flasher\Prime\FlasherInterface', 'flasher'); -} +declare(strict_types=1); + +use Flasher\Prime\Asset\AssetManager; +use Flasher\Prime\EventDispatcher\EventDispatcher; +use Flasher\Prime\EventDispatcher\EventListener\ApplyPresetListener; +use Flasher\Prime\EventDispatcher\EventListener\NotificationLoggerListener; +use Flasher\Prime\EventDispatcher\EventListener\TranslationListener; +use Flasher\Prime\Factory\NotificationFactory; +use Flasher\Prime\Flasher; +use Flasher\Prime\FlasherInterface; +use Flasher\Prime\Http\Csp\ContentSecurityPolicyHandler; +use Flasher\Prime\Http\Csp\NonceGenerator; +use Flasher\Prime\Http\RequestExtension; +use Flasher\Prime\Http\ResponseExtension; +use Flasher\Prime\Response\Resource\ResourceManager; +use Flasher\Prime\Response\ResponseManager; +use Flasher\Prime\Storage\Filter\FilterFactory; +use Flasher\Prime\Storage\Storage; +use Flasher\Prime\Storage\StorageManager; +use Flasher\Symfony\Command\InstallCommand; +use Flasher\Symfony\Component\FlasherComponent; +use Flasher\Symfony\EventListener\FlasherListener; +use Flasher\Symfony\EventListener\SessionListener; +use Flasher\Symfony\Factory\NotificationFactoryLocator; +use Flasher\Symfony\Profiler\FlasherDataCollector; +use Flasher\Symfony\Storage\SessionBag; +use Flasher\Symfony\Template\TwigTemplateEngine; +use Flasher\Symfony\Translation\Translator; +use Flasher\Symfony\Twig\FlasherTwigExtension; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +use function Symfony\Component\DependencyInjection\Loader\Configurator\inline_service; +use function Symfony\Component\DependencyInjection\Loader\Configurator\param; +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; +use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_locator; + +return static function (ContainerConfigurator $container): void { + $container->services() + ->set('flasher', Flasher::class) + ->public() + ->args([ + param('flasher.default'), + inline_service(NotificationFactoryLocator::class) + ->args([tagged_locator('flasher.factory', indexAttribute: 'alias')]), + service('flasher.response_manager'), + service('flasher.storage_manager'), + ]) + ->alias(FlasherInterface::class, 'flasher') + + ->set('flasher.flasher_listener', FlasherListener::class) + ->args([ + inline_service(ResponseExtension::class) + ->args([ + service('flasher'), + service('flasher.csp_handler'), + param('flasher.excluded_paths'), + ]), + ]) + ->tag('kernel.event_subscriber') + + ->set('flasher.twig_extension', FlasherTwigExtension::class) + ->args([service('flasher')]) + ->tag('twig.extension') + + ->set('flasher.session_listener', SessionListener::class) + ->args([ + inline_service(RequestExtension::class) + ->args([ + service('flasher'), + param('flasher.flash_bag'), + ]), + ]) + ->tag('kernel.event_subscriber') + + ->set('flasher.notification_logger_listener', NotificationLoggerListener::class) + ->tag('flasher.event_listener') + ->tag('kernel.reset', ['method' => 'reset']) + + ->set('flasher.translation_listener', TranslationListener::class) + ->args([service('flasher.translator')->nullOnInvalid()]) + ->tag('flasher.event_listener') + + ->set('flasher.preset_listener', ApplyPresetListener::class) + ->args([param('flasher.presets')]) + ->tag('flasher.event_listener') + + ->set('flasher.install_command', InstallCommand::class) + ->args([service('flasher.asset_manager')]) + ->tag('console.command') + + ->set('flasher.flasher_component', FlasherComponent::class) + ->tag('twig.component', [ + 'key' => 'flasher', + 'template' => '@FlasherSymfony/components/flasher.html.twig', + 'attributesVar' => 'attributes', + ]) + + ->set('flasher.notification_factory', NotificationFactory::class) + ->abstract() + ->args([service('flasher.storage_manager')]) + + ->set('flasher.storage', Storage::class) + ->args([service('flasher.storage_bag')]) + + ->set('flasher.storage_bag', SessionBag::class) + ->args([service('request_stack')]) + + ->set('flasher.event_dispatcher', EventDispatcher::class) + + ->set('flasher.filter_factory', FilterFactory::class) + + ->set('flasher.storage_manager', StorageManager::class) + ->args([ + service('flasher.storage'), + service('flasher.event_dispatcher'), + service('flasher.filter_factory'), + param('flasher.filter'), + ]) + + ->set('flasher.template_engine', TwigTemplateEngine::class) + ->args([service('twig')->nullOnInvalid()]) + + ->set('flasher.resource_manager', ResourceManager::class) + ->args([ + service('flasher.template_engine'), + service('flasher.asset_manager'), + param('flasher.main_script'), + param('flasher.resources'), + ]) + + ->set('flasher.response_manager', ResponseManager::class) + ->args([ + service('flasher.resource_manager'), + service('flasher.storage_manager'), + service('flasher.event_dispatcher'), + ]) + + ->set('flasher.translator', Translator::class) + ->args([service('translator')->nullOnInvalid()]) + + ->set('flasher.csp_handler', ContentSecurityPolicyHandler::class) + ->args([inline_service(NonceGenerator::class)]) + + ->set('flasher.asset_manager', AssetManager::class) + ->args([ + param('flasher.public_dir'), + param('flasher.json_manifest_path'), + ]) + + ->set('flasher.data_collector', FlasherDataCollector::class) + ->args([ + service('flasher.notification_logger_listener'), + param('flasher'), + ]) + ->tag('data_collector', ['id' => 'flasher', 'template' => '@FlasherSymfony/profiler/flasher.html.twig', 'priority' => 0]) + ; +}; diff --git a/Resources/translations/flasher.ar.php b/Resources/translations/flasher.ar.php index 52e2752..084fd9e 100644 --- a/Resources/translations/flasher.ar.php +++ b/Resources/translations/flasher.ar.php @@ -1,10 +1,5 @@ - */ +declare(strict_types=1); -use Flasher\Prime\Translation\Messages; - -return Messages::$ar; +return Flasher\Prime\Translation\Messages::get('ar'); diff --git a/Resources/translations/flasher.de.php b/Resources/translations/flasher.de.php new file mode 100644 index 0000000..57908c7 --- /dev/null +++ b/Resources/translations/flasher.de.php @@ -0,0 +1,5 @@ + - */ +declare(strict_types=1); -use Flasher\Prime\Translation\Messages; - -return Messages::$en; +return Flasher\Prime\Translation\Messages::get('en'); diff --git a/Resources/translations/flasher.es.php b/Resources/translations/flasher.es.php new file mode 100644 index 0000000..f7fe97e --- /dev/null +++ b/Resources/translations/flasher.es.php @@ -0,0 +1,5 @@ + - */ +declare(strict_types=1); -use Flasher\Prime\Translation\Messages; - -return Messages::$fr; +return Flasher\Prime\Translation\Messages::get('fr'); diff --git a/Resources/translations/flasher.pt.php b/Resources/translations/flasher.pt.php new file mode 100644 index 0000000..688f1ef --- /dev/null +++ b/Resources/translations/flasher.pt.php @@ -0,0 +1,5 @@ + + +
+ +
+ diff --git a/Resources/views/components/flasher.html.twig b/Resources/views/components/flasher.html.twig new file mode 100644 index 0000000..360776e --- /dev/null +++ b/Resources/views/components/flasher.html.twig @@ -0,0 +1,3 @@ +
+ {{ flasher_render() }} +
diff --git a/Resources/views/profiler/_notifications_table.html.twig b/Resources/views/profiler/_notifications_table.html.twig new file mode 100644 index 0000000..8f0cc42 --- /dev/null +++ b/Resources/views/profiler/_notifications_table.html.twig @@ -0,0 +1,30 @@ + + + + + + + + + + + + + {% for envelope in envelopes %} + + + + + + + + + {% endfor %} + +
#PluginTypeTitleMessageOptions
{{ loop.index }}{{ envelope.metadata.plugin }}{{ envelope.type }}{{ envelope.title }}{{ envelope.message }} + {% if envelope.options is not empty %} + {{ profiler_dump(envelope.options) }} + {% else %} + No Options + {% endif %} +
diff --git a/Resources/views/profiler/flasher.html.twig b/Resources/views/profiler/flasher.html.twig new file mode 100644 index 0000000..1157f9d --- /dev/null +++ b/Resources/views/profiler/flasher.html.twig @@ -0,0 +1,184 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% macro logo() %} + + PHPFlasher + +{% endmacro %} + +{% block toolbar %} + {% import _self as macros %} + + {% set displayedEnvelopes = collector.displayedEnvelopes %} + {% set dispatchedEnvelopes = collector.dispatchedEnvelopes %} + + {% set totalDispatched = dispatchedEnvelopes|length %} + {% set totalDisplayed = displayedEnvelopes|length %} + + {% if totalDisplayed > 0 %} + {# Initialize type counts #} + {% set typeCounts = {} %} + {% for envelope in displayedEnvelopes %} + {% set type = envelope.type|default('info')|lower %} + {% set typeCounts = typeCounts | merge({ (type): (typeCounts[type]|default(0) + 1) }) %} + {% endfor %} + + {% set icon %} + {{ macros.logo() }} + + {{ source('@FlasherSymfony/profiler/flasher.svg') }} + + {% if totalDisplayed == totalDispatched %} + {{ totalDisplayed }} + {% else %} + {{ totalDisplayed }}/{{ totalDispatched }} + {% endif %} + + {% endset %} + + {% set text %} +
+ Notifications Displayed + {{ totalDisplayed }} +
+ + {% if totalDispatched != totalDisplayed %} +
+ Notifications Dispatched: + {{ totalDispatched }} +
+ {% endif %} + + {% if totalDisplayed > totalDispatched %} +
+ Note: Some notifications are from previous requests. +
+ {% endif %} + + {% for type, count in typeCounts %} +
+ {{ type|capitalize }} + {{ count }} +
+ {% endfor %} + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} + {% endif %} +{% endblock %} + +{% block menu %} + {% import _self as macros %} + + {% set totalDisplayed = collector.displayedEnvelopes|length %} + {% set totalDispatched = collector.dispatchedEnvelopes|length %} + + + {{ source('@FlasherSymfony/profiler/flasher.svg') }} + {{ macros.logo() }} + {% if totalDisplayed > 0 %} + + {% if totalDisplayed == totalDispatched %} + {{ totalDisplayed }} + {% else %} + {{ totalDisplayed }}/{{ totalDispatched }} + {% endif %} + + {% endif %} + +{% endblock %} + +{% block panel %} + {% set displayedEnvelopes = collector.displayedEnvelopes %} + {% set dispatchedEnvelopes = collector.dispatchedEnvelopes %} + {% set totalNotifications = dispatchedEnvelopes|length %} + {% set displayedNotifications = displayedEnvelopes|length %} + {% set config = collector.config %} + {% set versions = collector.versions %} + +

PHPFlasher Notifications

+ + {% if totalNotifications == 0 %} +
+

No notifications have been dispatched.

+
+ {% else %} +
+
+

Notifications {{ displayedNotifications }}/{{ totalNotifications }}

+
+ {% if displayedNotifications > totalNotifications %} +
+

The number of displayed notifications is greater than the number of dispatched notifications. This may happen if notifications are stored in the session from previous requests.

+
+ {% endif %} + +

Displayed Notifications

+ {{ include('@FlasherSymfony/profiler/_notifications_table.html.twig', { 'envelopes': displayedEnvelopes }) }} + + {% if totalNotifications > displayedNotifications %} +

Remaining Notifications

+ {% set remainingNotifications = dispatchedEnvelopes|slice(displayedNotifications) %} + {{ include('@FlasherSymfony/profiler/_notifications_table.html.twig', { 'envelopes': remainingNotifications }) }} + {% endif %} +
+
+ +
+

Debug

+
+

Version Information

+
    +
  • PHPFlasher Version: {{ versions.php_flasher }}
  • +
  • PHP Version: {{ versions.php }}
  • +
  • Symfony Version: {{ versions.symfony }}
  • +
+ +

Configuration

+ {{ profiler_dump(config, maxDepth=10) }} +
+
+
+ {% endif %} +{% endblock %} + +{% block head %} + {{ parent() }} + +{% endblock %} diff --git a/Resources/views/profiler/flasher.svg b/Resources/views/profiler/flasher.svg new file mode 100644 index 0000000..291f1fb --- /dev/null +++ b/Resources/views/profiler/flasher.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/Resources/views/tailwindcss.html.twig b/Resources/views/tailwindcss.html.twig new file mode 100644 index 0000000..3074b17 --- /dev/null +++ b/Resources/views/tailwindcss.html.twig @@ -0,0 +1,52 @@ +{% if 'success' == envelope.type %} + {% set title = 'Success' %} + {% set text_color = 'text-green-600' %} + {% set ring_color = 'ring-green-300' %} + {% set background_color = 'bg-green-600' %} + {% set progress_background_color = 'bg-green-100' %} + {% set border_color = 'border-green-600' %} + {% set icon = '' %} +{% elseif 'error' == envelope.type %} + {% set title = 'Error' %} + {% set text_color = 'text-red-600' %} + {% set ring_color = 'ring-red-300' %} + {% set background_color = 'bg-red-600' %} + {% set progress_background_color = 'bg-red-100' %} + {% set border_color = 'border-red-600' %} + {% set icon = '' %} +{% elseif 'warning' == envelope.type %} + {% set title = 'Warning' %} + {% set text_color = 'text-yellow-600' %} + {% set ring_color = 'ring-yellow-300' %} + {% set background_color = 'bg-yellow-600' %} + {% set progress_background_color = 'bg-yellow-100' %} + {% set border_color = 'border-yellow-600' %} + {% set icon = '' %} +{% else %} + {% set title = 'Info' %} + {% set text_color = 'text-blue-600' %} + {% set ring_color = 'ring-blue-300' %} + {% set background_color = 'bg-blue-600' %} + {% set progress_background_color = 'bg-blue-100' %} + {% set border_color = 'border-blue-600' %} + {% set icon = '' %} +{% endif %} + +
+
+
+ {{ icon | raw }} +
+
+

+ {{ title | trans }} +

+

+ {{ envelope.message }} +

+
+
+
+ +
+
diff --git a/Resources/views/tailwindcss_bg.html.twig b/Resources/views/tailwindcss_bg.html.twig new file mode 100644 index 0000000..702e4d8 --- /dev/null +++ b/Resources/views/tailwindcss_bg.html.twig @@ -0,0 +1,48 @@ +{% if 'success' == envelope.type %} + {% set title = 'Success' %} + {% set text_color = 'text-green-700' %} + {% set background_color = 'bg-green-50' %} + {% set progress_background_color = 'bg-green-200' %} + {% set border_color = 'border-green-600' %} + {% set icon = '' %} +{% elseif 'error' == envelope.type %} + {% set title = 'Error' %} + {% set text_color = 'text-red-700' %} + {% set background_color = 'bg-red-50' %} + {% set progress_background_color = 'bg-red-200' %} + {% set border_color = 'border-red-600' %} + {% set icon = '' %} +{% elseif 'warning' == envelope.type %} + {% set title = 'Warning' %} + {% set text_color = 'text-yellow-700' %} + {% set background_color = 'bg-yellow-50' %} + {% set progress_background_color = 'bg-yellow-200' %} + {% set border_color = 'border-yellow-600' %} + {% set icon = '' %} +{% else %} + {% set title = 'Info' %} + {% set text_color = 'text-blue-700' %} + {% set background_color = 'bg-blue-50' %} + {% set progress_background_color = 'bg-blue-200' %} + {% set border_color = 'border-blue-600' %} + {% set icon = '' %} +{% endif %} + +
+
+
+ {{ icon | raw }} +
+
+

+ {{ title | trans }} +

+

+ {{ envelope.message }} +

+
+
+
+ +
+
diff --git a/Resources/views/tailwindcss_r.html.twig b/Resources/views/tailwindcss_r.html.twig new file mode 100644 index 0000000..4227a28 --- /dev/null +++ b/Resources/views/tailwindcss_r.html.twig @@ -0,0 +1,53 @@ +{% if 'success' == envelope.type %} + {% set title = 'Success' %} + {% set text_color = 'text-green-600' %} + {% set ring_color = 'ring-green-300' %} + {% set background_color = 'bg-green-600' %} + {% set progress_background_color = 'bg-green-100' %} + {% set border_color = 'border-green-600' %} + {% set icon = '' %} +{% elseif 'error' == envelope.type %} + {% set title = 'Error' %} + {% set text_color = 'text-red-600' %} + {% set ring_color = 'ring-red-300' %} + {% set background_color = 'bg-red-600' %} + {% set progress_background_color = 'bg-red-100' %} + {% set border_color = 'border-red-600' %} + {% set icon = '' %} +{% elseif 'warning' == envelope.type %} + {% set title = 'Warning' %} + {% set text_color = 'text-yellow-600' %} + {% set ring_color = 'ring-yellow-300' %} + {% set background_color = 'bg-yellow-600' %} + {% set progress_background_color = 'bg-yellow-100' %} + {% set border_color = 'border-yellow-600' %} + {% set icon = '' %} +{% else %} + {% set title = 'Info' %} + {% set text_color = 'text-blue-600' %} + {% set ring_color = 'ring-blue-300' %} + {% set background_color = 'bg-blue-600' %} + {% set progress_background_color = 'bg-blue-100' %} + {% set border_color = 'border-blue-600' %} + {% set icon = '' %} +{% endif %} + + +
+
+
+ {{ icon | raw }} +
+
+

+ {{ title | trans }} +

+

+ {{ envelope.message }} +

+
+
+
+ +
+
diff --git a/Storage/FallbackSession.php b/Storage/FallbackSession.php index 96e8482..768fe3b 100644 --- a/Storage/FallbackSession.php +++ b/Storage/FallbackSession.php @@ -1,31 +1,36 @@ */ - public function get($name, $default = null) + private static array $storage = []; + + public function get(string $name, mixed $default = null): mixed { - return array_key_exists($name, self::$storage) - ? self::$storage[$name] - : $default; + return \array_key_exists($name, self::$storage) ? self::$storage[$name] : $default; } - /** - * @param string $name - * @param mixed $value - * - * @return void - */ - public function set($name, $value) + public function set(string $name, mixed $value): void { self::$storage[$name] = $value; } diff --git a/Storage/FallbackSessionInterface.php b/Storage/FallbackSessionInterface.php new file mode 100644 index 0000000..bf7b790 --- /dev/null +++ b/Storage/FallbackSessionInterface.php @@ -0,0 +1,38 @@ + - */ +declare(strict_types=1); namespace Flasher\Symfony\Storage; +use Flasher\Prime\Notification\Envelope; use Flasher\Prime\Storage\Bag\BagInterface; use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session as LegacySession; use Symfony\Component\HttpFoundation\Session\SessionInterface; -final class SessionBag implements BagInterface +/** + * SessionBag - Symfony session storage for PHPFlasher notifications. + * + * This class implements PHPFlasher's storage interface using Symfony's session + * system, providing persistence for notifications across requests. It includes + * fallback behavior for stateless contexts. + * + * Design patterns: + * - Adapter Pattern: Adapts Symfony's session to PHPFlasher's storage interface + * - Strategy Pattern: Uses different storage strategies based on context + * - Fallback Strategy: Falls back to in-memory storage when session unavailable + * - Repository Pattern: Provides CRUD operations for notification storage + */ +final readonly class SessionBag implements BagInterface { - const ENVELOPES_NAMESPACE = 'flasher::envelopes'; - /** - * @var RequestStack|SessionInterface + * Session key for storing notification envelopes. */ - private $session; + public const ENVELOPES_NAMESPACE = 'flasher::envelopes'; /** - * @var FallbackSession + * Fallback storage for contexts where session is unavailable. */ - private $fallbackSession; + private FallbackSessionInterface $fallbackSession; /** - * @param RequestStack|SessionInterface $session + * Creates a new SessionBag instance. + * + * @param RequestStack $requestStack Symfony's request stack for accessing session + * @param FallbackSessionInterface|null $fallbackSession Optional custom fallback storage */ - public function __construct($session) + public function __construct(private RequestStack $requestStack, ?FallbackSessionInterface $fallbackSession = null) { - $this->session = $session; - $this->fallbackSession = new FallbackSession(); + $this->fallbackSession = $fallbackSession ?: new FallbackSession(); } /** * {@inheritdoc} + * + * Gets all notification envelopes from storage. + * + * @return Envelope[] The stored notification envelopes */ - public function get() + public function get(): array { - return $this->session()->get(self::ENVELOPES_NAMESPACE, array()); // @phpstan-ignore-line + $session = $this->getSession(); + + /** @var Envelope[] $envelopes */ + $envelopes = $session->get(self::ENVELOPES_NAMESPACE, []); + + return $envelopes; } /** * {@inheritdoc} + * + * Stores notification envelopes in storage. + * + * @param array $envelopes The notification envelopes to store */ - public function set(array $envelopes) + public function set(array $envelopes): void { - $this->session()->set(self::ENVELOPES_NAMESPACE, $envelopes); + $session = $this->getSession(); + + $session->set(self::ENVELOPES_NAMESPACE, $envelopes); } /** - * @return SessionInterface + * Gets the appropriate session storage implementation. + * + * Uses Symfony session if available and request is not stateless, + * otherwise falls back to the fallback session implementation. + * + * @return SessionInterface|FallbackSessionInterface The storage implementation */ - private function session() + private function getSession(): SessionInterface|FallbackSessionInterface { - if ($this->session instanceof SessionInterface || $this->session instanceof LegacySession) { // @phpstan-ignore-line - return $this->session; // @phpstan-ignore-line - } - try { - if (method_exists($this->session, 'getSession')) { - $session = $this->session->getSession(); - } else { - $session = $this->session->getCurrentRequest()->getSession(); - } + $request = $this->requestStack->getCurrentRequest(); - $isStateless = $this->session->getCurrentRequest()->attributes->has('_stateless'); - - if (null !== $session && !$isStateless) { - return $this->session = $session; + if ($request && !$request->attributes->get('_stateless', false)) { + return $this->requestStack->getSession(); } - - return $this->fallbackSession; - } catch (SessionNotFoundException $e) { - return $this->fallbackSession; + } catch (SessionNotFoundException) { } + + return $this->fallbackSession; } } diff --git a/Support/Bundle.php b/Support/Bundle.php deleted file mode 100644 index 8de3215..0000000 --- a/Support/Bundle.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ - -namespace Flasher\Symfony\Support; - -use Flasher\Prime\Plugin\PluginInterface; -use Flasher\Symfony\Bridge\FlasherBundle; - -abstract class Bundle extends FlasherBundle -{ - /** - * @return PluginInterface - */ - abstract public function createPlugin(); - - public function getConfigurationFile() - { - return rtrim($this->getResourcesDir(), '/').'/config/config.yaml'; - } - - protected function getFlasherContainerExtension() - { - return new Extension($this->createPlugin()); - } - - /** - * @return string - */ - protected function getResourcesDir() - { - $r = new \ReflectionClass($this); - - return pathinfo($r->getFileName() ?: '', PATHINFO_DIRNAME).'/Resources/'; - } -} diff --git a/Support/Configuration.php b/Support/Configuration.php deleted file mode 100644 index 39702e7..0000000 --- a/Support/Configuration.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ - -namespace Flasher\Symfony\Support; - -use Flasher\Prime\Plugin\PluginInterface; -use Flasher\Symfony\Bridge\DependencyInjection\FlasherConfiguration; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; - -class Configuration extends FlasherConfiguration -{ - /** - * @var PluginInterface - */ - private $plugin; - - public function __construct(PluginInterface $plugin) - { - $this->plugin = $plugin; - } - - public function getFlasherConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder($this->plugin->getName()); - - $rootNode = method_exists($treeBuilder, 'getRootNode') - ? $treeBuilder->getRootNode() - : $treeBuilder->root($this->plugin->getName()); // @phpstan-ignore-line - - $plugin = $this->plugin; - $rootNode - ->beforeNormalization() - ->always(function ($v) use ($plugin) { - return $plugin->normalizeConfig($v); - }) - ->end() - ->children() - ->arrayNode('scripts') - ->prototype('variable')->end() - ->defaultValue($this->plugin->getScripts()) - ->end() - ->arrayNode('styles') - ->prototype('variable')->end() - ->defaultValue($this->plugin->getStyles()) - ->end() - ->arrayNode('options') - ->prototype('variable')->end() - ->ignoreExtraKeys(false) - ->defaultValue($this->plugin->getOptions()) - ->end() - ->end() - ; - - return $treeBuilder; - } -} diff --git a/Support/Extension.php b/Support/Extension.php deleted file mode 100644 index 25a47e4..0000000 --- a/Support/Extension.php +++ /dev/null @@ -1,116 +0,0 @@ - - */ - -namespace Flasher\Symfony\Support; - -use Flasher\Prime\Plugin\PluginInterface; -use Flasher\Symfony\Bridge\Bridge; -use Flasher\Symfony\Bridge\DependencyInjection\FlasherExtension; -use Symfony\Component\Config\Definition\ConfigurationInterface; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\DefinitionDecorator; - -final class Extension extends FlasherExtension implements CompilerPassInterface -{ - /** - * @var PluginInterface - */ - private $plugin; - - public function __construct(PluginInterface $plugin) - { - $this->plugin = $plugin; - } - - /** - * {@inheritdoc} - * - * @param array> $configs - * - * @return void - */ - public function load(array $configs, ContainerBuilder $container) - { - /** @var ChildDefinition $definition */ - $definition = class_exists('Symfony\Component\DependencyInjection\ChildDefinition') - ? new ChildDefinition('flasher.notification_factory') - : new DefinitionDecorator('flasher.notification_factory'); // @phpstan-ignore-line - - $definition - ->setClass($this->plugin->getFactory()) - ->setPublic(true) - ->addTag('flasher.factory', array('alias' => $this->plugin->getAlias())); - - $identifier = $this->plugin->getServiceID(); - $container->setDefinition($identifier, $definition); - - if (Bridge::canLoadAliases()) { - $container->setAlias($this->plugin->getFactory(), $identifier); - } - } - - /** - * {@inheritdoc} - */ - public function getFlasherAlias() - { - return $this->plugin->getName(); - } - - /** - * Returns extension configuration. - * - * @param array> $config - * - * @return ConfigurationInterface|null - */ - public function getConfiguration(array $config, ContainerBuilder $container) - { - return new Configuration($this->plugin); - } - - /** - * {@inheritdoc} - * - * @return void - */ - public function process(ContainerBuilder $container) - { - $configs = $this->processConfiguration( - new Configuration($this->plugin), - $container->getExtensionConfig($this->plugin->getName()) - ); - - $this->processResourceConfiguration($configs, $container); - } - - /** - * @param array $configs - * - * @return void - */ - protected function processResourceConfiguration(array $configs, ContainerBuilder $container) - { - if (!$container->has('flasher.resource_manager')) { - return; - } - - $definition = $container->getDefinition('flasher.resource_manager'); - $handler = $this->plugin->getAlias(); - - $scripts = isset($configs['scripts']) ? $configs['scripts'] : array(); - $definition->addMethodCall('addScripts', array($handler, $scripts)); - - $styles = isset($configs['styles']) ? $configs['styles'] : array(); - $definition->addMethodCall('addStyles', array($handler, $styles)); - - $options = isset($configs['options']) ? $configs['options'] : array(); - $definition->addMethodCall('addOptions', array($handler, $options)); - } -} diff --git a/Support/PluginBundle.php b/Support/PluginBundle.php new file mode 100644 index 0000000..f6a7d2c --- /dev/null +++ b/Support/PluginBundle.php @@ -0,0 +1,129 @@ + $config The processed bundle configuration + * @param ContainerConfigurator $container The container configurator + * @param ContainerBuilder $builder The container builder + */ + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + if ($this instanceof FlasherSymfonyBundle) { + return; + } + + $plugin = $this->createPlugin(); + $identifier = $plugin->getServiceId(); + + $container->services() + ->set($identifier, $plugin->getFactory()) + ->parent('flasher.notification_factory') + ->tag('flasher.factory', ['alias' => $plugin->getAlias()]) + ->public() + ; + + foreach ((array) $plugin->getServiceAliases() as $alias) { + $builder->setAlias($alias, $identifier); + } + } + + /** + * Prepends default plugin configuration for Flasher. + * + * This method adds the plugin's scripts, styles, and options to the Flasher + * configuration before the container is compiled. + * + * @param ContainerConfigurator $container The container configurator + * @param ContainerBuilder $builder The container builder + */ + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + if ($this instanceof FlasherSymfonyBundle) { + return; + } + + $plugin = $this->createPlugin(); + + $builder->prependExtensionConfig('flasher', [ + 'plugins' => [ + $plugin->getAlias() => [ + 'scripts' => (array) $plugin->getScripts(), + 'styles' => (array) $plugin->getStyles(), + 'options' => $plugin->getOptions(), + ], + ], + ]); + } + + /** + * Gets the path to the plugin's configuration file. + * + * Returns the absolute path to the plugin's configuration file + * based on the bundle's path. + * + * @return string Absolute path to the configuration file + */ + public function getConfigurationFile(): string + { + return rtrim($this->getPath(), '/').'/Resources/config/config.yaml'; + } + + /** + * Gets the bundle's directory path. + * + * Uses reflection to determine the location of the bundle class file, + * then returns its directory. + * + * @return string The bundle directory path + */ + public function getPath(): string + { + if (!isset($this->path)) { + $reflected = new \ReflectionObject($this); + // assume the modern directory structure by default + $this->path = \dirname($reflected->getFileName() ?: ''); + } + + return $this->path; + } +} diff --git a/Support/PluginBundleInterface.php b/Support/PluginBundleInterface.php new file mode 100644 index 0000000..1b2850d --- /dev/null +++ b/Support/PluginBundleInterface.php @@ -0,0 +1,38 @@ + - */ +declare(strict_types=1); namespace Flasher\Symfony\Template; use Flasher\Prime\Template\TemplateEngineInterface; use Twig\Environment; -final class TwigTemplateEngine implements TemplateEngineInterface +/** + * TwigTemplateEngine - Adapter for Symfony's Twig template engine. + * + * This class adapts Symfony's Twig environment to PHPFlasher's template engine + * interface, enabling notification templates to be rendered using Twig. + * + * Design patterns: + * - Adapter Pattern: Adapts Twig to PHPFlasher's template interface + * - Null Object Pattern: Gracefully handles a missing Twig dependency + * - Bridge Pattern: Bridges PHPFlasher's templating needs with Symfony's templating system + */ +final readonly class TwigTemplateEngine implements TemplateEngineInterface { /** - * @var Environment + * Creates a new TwigTemplateEngine instance. + * + * @param Environment|null $twig The Twig environment or null if Twig is not available */ - private $engine; - - public function __construct(Environment $engine) + public function __construct(private ?Environment $twig = null) { - $this->engine = $engine; } - public function render($name, array $context = array()) + /** + * Renders a template using Twig. + * + * @param string $name The template name or path + * @param array $context The template variables + * + * @return string The rendered template + * + * @throws \LogicException If Twig is not available + */ + public function render(string $name, array $context = []): string { - return $this->engine->render($name, $context); + if (null === $this->twig) { + throw new \LogicException('The TwigBundle is not registered in your application. Try running "composer require symfony/twig-bundle".'); + } + + return $this->twig->render($name, $context); } } diff --git a/Translation/Translator.php b/Translation/Translator.php index 013b554..63a3fa1 100644 --- a/Translation/Translator.php +++ b/Translation/Translator.php @@ -1,48 +1,59 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\Translation; -use Flasher\Prime\Stamp\TranslationStamp; use Flasher\Prime\Translation\TranslatorInterface; use Symfony\Component\Translation\TranslatorBagInterface; use Symfony\Contracts\Translation\TranslatorInterface as SymfonyTranslatorInterface; -final class Translator implements TranslatorInterface +/** + * Translator - Adapter for Symfony's translation service. + * + * This class adapts Symfony's translation system to PHPFlasher's TranslatorInterface, + * enabling notification messages to be translated using Symfony's translation capabilities. + * It searches for messages in multiple domains with cascading fallbacks. + * + * Design patterns: + * - Adapter Pattern: Adapts Symfony's translator interface to PHPFlasher's interface + * - Facade Pattern: Simplifies the translation process with a unified interface + * - Chain of Responsibility: Tries multiple translation domains in sequence + */ +final readonly class Translator implements TranslatorInterface { /** - * @var SymfonyTranslatorInterface - */ - private $translator; - - /** - * @param SymfonyTranslatorInterface $translator + * Creates a new Translator instance. + * + * @param SymfonyTranslatorInterface $translator The Symfony translator service */ - public function __construct($translator) + public function __construct(private SymfonyTranslatorInterface $translator) { - $this->translator = $translator; } /** - * {@inheritdoc} + * Translates a message using Symfony's translation system. + * + * This method attempts to translate the message in the following order: + * 1. In the 'flasher' domain (flasher-specific translations) + * 2. In the 'messages' domain (application-wide translations) + * 3. Returns the original ID if no translation is found + * + * @param string $id The message ID or key + * @param array $parameters The translation parameters + * @param string|null $locale The locale or null to use the default + * + * @return string The translated string */ - public function translate($id, $parameters = array(), $locale = null) + public function translate(string $id, array $parameters = [], ?string $locale = null): string { - $order = TranslationStamp::parametersOrder($parameters, $locale); - $parameters = $this->addPrefixedParams($order['parameters']); - $locale = $order['locale']; - if (!$this->translator instanceof TranslatorBagInterface) { return $this->translator->trans($id, $parameters, 'flasher', $locale); } $catalogue = $this->translator->getCatalogue($locale); - foreach (array('flasher', 'messages') as $domain) { + foreach (['flasher', 'messages'] as $domain) { if ($catalogue->has($id, $domain)) { return $this->translator->trans($id, $parameters, $domain, $locale); } @@ -52,26 +63,18 @@ public function translate($id, $parameters = array(), $locale = null) } /** - * {@inheritDoc} - */ - public function getLocale() - { - return $this->translator->getLocale(); - } - - /** - * @param array $parameters + * Gets the current locale from Symfony's translator. * - * @return array + * Falls back to system default locale if translator doesn't provide one. + * + * @return string The current locale code */ - private function addPrefixedParams(array $parameters) + public function getLocale(): string { - foreach ($parameters as $key => $value) { - if (0 !== strpos($key, ':')) { - $parameters[':'.$key] = $value; - } + if (method_exists($this->translator, 'getLocale')) { // @phpstan-ignore-line + return $this->translator->getLocale(); } - return $parameters; + return class_exists(\Locale::class) ? \Locale::getDefault() : 'en'; } } diff --git a/Twig/FlasherTwigExtension.php b/Twig/FlasherTwigExtension.php index ca098a4..fcbc020 100644 --- a/Twig/FlasherTwigExtension.php +++ b/Twig/FlasherTwigExtension.php @@ -1,32 +1,59 @@ - */ +declare(strict_types=1); namespace Flasher\Symfony\Twig; -use Flasher\Symfony\Bridge\Twig\FlasherTwigExtension as BaseFlasherTwigExtension; +use Flasher\Prime\FlasherInterface; +use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -final class FlasherTwigExtension extends BaseFlasherTwigExtension +/** + * FlasherTwigExtension - Twig extension for rendering notifications. + * + * This class provides Twig functions that allow notification rendering + * directly from Twig templates. It exposes PHPFlasher's rendering + * capabilities to template files. + * + * Design patterns: + * - Extension Pattern: Extends Twig's functionality + * - Adapter Pattern: Adapts PHPFlasher's API for Twig templates + * - Delegation: Delegates actual rendering to the Flasher service + */ +final class FlasherTwigExtension extends AbstractExtension { /** - * @return TwigFunction[] + * Creates a new FlasherTwigExtension instance. + * + * @param FlasherInterface $flasher The PHPFlasher service + */ + public function __construct(private readonly FlasherInterface $flasher) + { + } + + /** + * Returns the Twig functions provided by this extension. + * + * @return TwigFunction[] Array of Twig functions */ - public function getFlasherFunctions() + public function getFunctions(): array { - return array( - new TwigFunction('flasher_render', array($this, 'render')), - ); + return [ + new TwigFunction('flasher_render', $this->render(...), ['is_safe' => ['html']]), + ]; } /** - * @return string + * Renders the flash notifications based on the specified criteria, presenter, and context. + * + * @param array $criteria the criteria to filter the notifications + * @param "html"|"json"|string $presenter The presenter format for rendering the notifications (e.g., 'html', 'json'). + * @param array $context additional context or options for rendering + * + * @return mixed The rendered output (HTML string, JSON string, etc.) */ - public function render() + public function render(array $criteria = [], string $presenter = 'html', array $context = []): mixed { - return ''; + return $this->flasher->render($presenter, $criteria, $context); } } diff --git a/composer.json b/composer.json index 1e05002..131fa52 100644 --- a/composer.json +++ b/composer.json @@ -1,54 +1,43 @@ { "name": "php-flasher/flasher-symfony", - "description": "PHPFlasher - A powerful & easy-to-use package for adding flash messages to Laravel or Symfony projects. Provides feedback to users, improves engagement & enhances user experience. Intuitive design for beginners & experienced developers. A reliable, flexible solution.", - "license": "MIT", "type": "symfony-bundle", + "license": "MIT", + "homepage": "https://php-flasher.io", + "description": "Integrate flash notifications into Symfony projects effortlessly with PHPFlasher. Improve user experience and application feedback loops easily.", "keywords": [ - "php-flasher", - "flash-messages", - "notification-system", - "user-feedback", - "toastr", - "sweetalert", - "pnotify", - "noty", - "notyf", - "desktop-notifications", - "php", - "laravel", "symfony", - "javascript", - "yoeunes", - "framework-agnostic", - "phpstorm-auto-complete", - "custom-adapter", - "user-experience", - "rtl", - "dark-mode" + "php", + "flash-notifications", + "phpflasher", + "user-feedback", + "open-source" ], + "support": { + "issues": "https://github.com/php-flasher/php-flasher/issues", + "source": "https://github.com/php-flasher/php-flasher" + }, "authors": [ { - "name": "Younes KHOUBZA", - "email": "younes.khoubza@gmail.com", - "homepage": "https://www.linkedin.com/in/younes-khoubza", + "name": "Younes ENNAJI", + "email": "younes.ennaji.pro@gmail.com", + "homepage": "https://www.linkedin.com/in/younes--ennaji/", "role": "Developer" } ], - "homepage": "https://php-flasher.io", + "minimum-stability": "dev", + "prefer-stable": true, "require": { - "php": ">=5.3", - "php-flasher/flasher": "^1.15.14", - "symfony/config": "^2.0 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/console": "^2.0 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^2.0 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/http-kernel": "^2.0 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "php": ">=8.2", + "php-flasher/flasher": "^2.2.0", + "symfony/config": "^7.0", + "symfony/console": "^7.0", + "symfony/dependency-injection": "^7.0", + "symfony/http-kernel": "^7.0" }, "suggest": { "symfony/translation": "To translate flash messages, title and presets", - "symfony/twig-bundle": "To create custom themes using twig templates" + "symfony/ux-twig-component": "To utilize and interact with flash messages components in Twig templates" }, - "minimum-stability": "stable", - "prefer-stable": true, "autoload": { "psr-4": { "Flasher\\Symfony\\": ""