diff --git a/examples/config/neuron.yaml b/examples/config/neuron.yaml index 154004f..b6fc488 100644 --- a/examples/config/neuron.yaml +++ b/examples/config/neuron.yaml @@ -55,3 +55,18 @@ member: # Email verification token expiration time (in minutes) # Tokens older than this will be considered invalid verification_token_expiration_minutes: 60 + +# Exception Handling Configuration +exceptions: + # Exceptions that should pass through to application-level handlers + # instead of being caught by the framework's error handler. + # This is useful for custom error pages or specific exception handling logic. + passthrough: + # CMS-specific exceptions that require special handling (redirects, specific error pages) + - Neuron\Cms\Exceptions\UnauthenticatedException + - Neuron\Cms\Exceptions\EmailVerificationRequiredException + - Neuron\Cms\Exceptions\CsrfValidationException + # Additional exceptions can be added as needed + # - RuntimeException + # - LogicException + # - InvalidArgumentException diff --git a/resources/config/neuron.yaml b/resources/config/neuron.yaml index e8a072c..cf4fb9c 100644 --- a/resources/config/neuron.yaml +++ b/resources/config/neuron.yaml @@ -4,7 +4,6 @@ system: timezone: UTC base_path: resources - routes_path: resources/config logging: destination: \Neuron\Log\Destination\File @@ -82,3 +81,18 @@ member: # Email verification token expiration time (in minutes) # Tokens older than this will be considered invalid verification_token_expiration_minutes: 60 + +# Exception Handling Configuration +exceptions: + # Exceptions that should pass through to application-level handlers + # instead of being caught by the framework's error handler. + # This is useful for custom error pages or specific exception handling logic. + passthrough: + # CMS-specific exceptions that require special handling (redirects, specific error pages) + - Neuron\Cms\Exceptions\UnauthenticatedException + - Neuron\Cms\Exceptions\EmailVerificationRequiredException + - Neuron\Cms\Exceptions\CsrfValidationException + # Additional exceptions can be added as needed + # - RuntimeException + # - LogicException + # - InvalidArgumentException diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 754254a..1c71939 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -42,14 +42,6 @@ function boot( string $configPath ) : Application { - // Register CMS exceptions that should bubble up to application-level handlers - // These exceptions require special handling (redirects, specific error pages) - Registry::getInstance()->set( 'BubbleExceptions', [ - 'Neuron\\Cms\\Exceptions\\UnauthenticatedException', - 'Neuron\\Cms\\Exceptions\\EmailVerificationRequiredException', - 'Neuron\\Cms\\Exceptions\\CsrfValidationException' - ] ); - // Build and register the DI container BEFORE MVC boot // This ensures the container is available to initializers during boot try diff --git a/src/Cms/Controllers/Admin/Categories.php b/src/Cms/Controllers/Admin/Categories.php index fee3131..4eda93f 100644 --- a/src/Cms/Controllers/Admin/Categories.php +++ b/src/Cms/Controllers/Admin/Categories.php @@ -35,37 +35,23 @@ class Categories extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param ICategoryRepository|null $categoryRepository - * @param ICategoryCreator|null $categoryCreator - * @param ICategoryUpdater|null $categoryUpdater + * @param ICategoryRepository $categoryRepository + * @param ICategoryCreator $categoryCreator + * @param ICategoryUpdater $categoryUpdater */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?ICategoryRepository $categoryRepository = null, - ?ICategoryCreator $categoryCreator = null, - ?ICategoryUpdater $categoryUpdater = null + ICategoryRepository $categoryRepository, + ICategoryCreator $categoryCreator, + ICategoryUpdater $categoryUpdater ) { parent::__construct( $app, $settings, $sessionManager ); - if( $categoryRepository === null ) - { - throw new \InvalidArgumentException( 'ICategoryRepository must be injected' ); - } $this->_categoryRepository = $categoryRepository; - - if( $categoryCreator === null ) - { - throw new \InvalidArgumentException( 'ICategoryCreator must be injected' ); - } $this->_categoryCreator = $categoryCreator; - - if( $categoryUpdater === null ) - { - throw new \InvalidArgumentException( 'ICategoryUpdater must be injected' ); - } $this->_categoryUpdater = $categoryUpdater; } diff --git a/src/Cms/Controllers/Admin/Dashboard.php b/src/Cms/Controllers/Admin/Dashboard.php index 5aa9743..36ce7f4 100644 --- a/src/Cms/Controllers/Admin/Dashboard.php +++ b/src/Cms/Controllers/Admin/Dashboard.php @@ -4,6 +4,7 @@ use Neuron\Cms\Auth\SessionManager; use Neuron\Cms\Controllers\Content; +use Neuron\Cms\Enums\FlashMessageType; use Neuron\Core\Exceptions\NotFound; use Neuron\Data\Settings\SettingManager; use Neuron\Mvc\IMvcApplication; @@ -54,6 +55,8 @@ public function index( Request $request ): string ->description( 'Admin Dashboard' ) ->withCurrentUser() ->withCsrfToken() + ->with( 'Error', $this->getSessionManager()->getFlash( FlashMessageType::ERROR->value ) ) + ->with( 'Success', $this->getSessionManager()->getFlash( FlashMessageType::SUCCESS->value ) ) ->render( 'index', 'admin' ); } } diff --git a/src/Cms/Controllers/Admin/EventCategories.php b/src/Cms/Controllers/Admin/EventCategories.php index dd2d1c8..78507f5 100644 --- a/src/Cms/Controllers/Admin/EventCategories.php +++ b/src/Cms/Controllers/Admin/EventCategories.php @@ -35,38 +35,24 @@ class EventCategories extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IEventCategoryRepository|null $repository - * @param IEventCategoryCreator|null $creator - * @param IEventCategoryUpdater|null $updater + * @param IEventCategoryRepository $repository + * @param IEventCategoryCreator $creator + * @param IEventCategoryUpdater $updater * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IEventCategoryRepository $repository = null, - ?IEventCategoryCreator $creator = null, - ?IEventCategoryUpdater $updater = null + IEventCategoryRepository $repository, + IEventCategoryCreator $creator, + IEventCategoryUpdater $updater ) { parent::__construct( $app, $settings, $sessionManager ); - if( $repository === null ) - { - throw new \InvalidArgumentException( 'IEventCategoryRepository must be injected' ); - } $this->_repository = $repository; - - if( $creator === null ) - { - throw new \InvalidArgumentException( 'IEventCategoryCreator must be injected' ); - } $this->_creator = $creator; - - if( $updater === null ) - { - throw new \InvalidArgumentException( 'IEventCategoryUpdater must be injected' ); - } $this->_updater = $updater; } diff --git a/src/Cms/Controllers/Admin/Events.php b/src/Cms/Controllers/Admin/Events.php index 372872d..5fdb93d 100644 --- a/src/Cms/Controllers/Admin/Events.php +++ b/src/Cms/Controllers/Admin/Events.php @@ -39,45 +39,26 @@ class Events extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IEventRepository|null $eventRepository - * @param IEventCategoryRepository|null $categoryRepository - * @param IEventCreator|null $creator - * @param IEventUpdater|null $updater + * @param IEventRepository $eventRepository + * @param IEventCategoryRepository $categoryRepository + * @param IEventCreator $creator + * @param IEventUpdater $updater */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IEventRepository $eventRepository = null, - ?IEventCategoryRepository $categoryRepository = null, - ?IEventCreator $creator = null, - ?IEventUpdater $updater = null + IEventRepository $eventRepository, + IEventCategoryRepository $categoryRepository, + IEventCreator $creator, + IEventUpdater $updater ) { parent::__construct( $app, $settings, $sessionManager ); - if( $eventRepository === null ) - { - throw new \InvalidArgumentException( 'IEventRepository must be injected' ); - } $this->_eventRepository = $eventRepository; - - if( $categoryRepository === null ) - { - throw new \InvalidArgumentException( 'IEventCategoryRepository must be injected' ); - } $this->_categoryRepository = $categoryRepository; - - if( $creator === null ) - { - throw new \InvalidArgumentException( 'IEventCreator must be injected' ); - } $this->_creator = $creator; - - if( $updater === null ) - { - throw new \InvalidArgumentException( 'IEventUpdater must be injected' ); - } $this->_updater = $updater; } diff --git a/src/Cms/Controllers/Admin/Media.php b/src/Cms/Controllers/Admin/Media.php index 5de0a2d..c1f241c 100644 --- a/src/Cms/Controllers/Admin/Media.php +++ b/src/Cms/Controllers/Admin/Media.php @@ -35,30 +35,21 @@ class Media extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param CloudinaryUploader|null $uploader - * @param MediaValidator|null $validator + * @param CloudinaryUploader $uploader + * @param MediaValidator $validator * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?CloudinaryUploader $uploader = null, - ?MediaValidator $validator = null + CloudinaryUploader $uploader, + MediaValidator $validator ) { parent::__construct( $app, $settings, $sessionManager ); - if( $uploader === null ) - { - throw new \InvalidArgumentException( 'CloudinaryUploader must be injected' ); - } $this->_uploader = $uploader; - - if( $validator === null ) - { - throw new \InvalidArgumentException( 'MediaValidator must be injected' ); - } $this->_validator = $validator; } diff --git a/src/Cms/Controllers/Admin/Pages.php b/src/Cms/Controllers/Admin/Pages.php index 2ea2695..fb08a76 100644 --- a/src/Cms/Controllers/Admin/Pages.php +++ b/src/Cms/Controllers/Admin/Pages.php @@ -39,37 +39,23 @@ class Pages extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IPageRepository|null $pageRepository - * @param IPageCreator|null $pageCreator - * @param IPageUpdater|null $pageUpdater + * @param IPageRepository $pageRepository + * @param IPageCreator $pageCreator + * @param IPageUpdater $pageUpdater */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IPageRepository $pageRepository = null, - ?IPageCreator $pageCreator = null, - ?IPageUpdater $pageUpdater = null + IPageRepository $pageRepository, + IPageCreator $pageCreator, + IPageUpdater $pageUpdater ) { parent::__construct( $app, $settings, $sessionManager ); - if( $pageRepository === null ) - { - throw new \InvalidArgumentException( 'IPageRepository must be injected' ); - } $this->_pageRepository = $pageRepository; - - if( $pageCreator === null ) - { - throw new \InvalidArgumentException( 'IPageCreator must be injected' ); - } $this->_pageCreator = $pageCreator; - - if( $pageUpdater === null ) - { - throw new \InvalidArgumentException( 'IPageUpdater must be injected' ); - } $this->_pageUpdater = $pageUpdater; } diff --git a/src/Cms/Controllers/Admin/Posts.php b/src/Cms/Controllers/Admin/Posts.php index eb8dce4..c5c44cf 100644 --- a/src/Cms/Controllers/Admin/Posts.php +++ b/src/Cms/Controllers/Admin/Posts.php @@ -44,61 +44,32 @@ class Posts extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IPostRepository|null $postRepository - * @param ICategoryRepository|null $categoryRepository - * @param ITagRepository|null $tagRepository - * @param IPostCreator|null $postCreator - * @param IPostUpdater|null $postUpdater - * @param IPostDeleter|null $postDeleter + * @param IPostRepository $postRepository + * @param ICategoryRepository $categoryRepository + * @param ITagRepository $tagRepository + * @param IPostCreator $postCreator + * @param IPostUpdater $postUpdater + * @param IPostDeleter $postDeleter */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IPostRepository $postRepository = null, - ?ICategoryRepository $categoryRepository = null, - ?ITagRepository $tagRepository = null, - ?IPostCreator $postCreator = null, - ?IPostUpdater $postUpdater = null, - ?IPostDeleter $postDeleter = null + IPostRepository $postRepository, + ICategoryRepository $categoryRepository, + ITagRepository $tagRepository, + IPostCreator $postCreator, + IPostUpdater $postUpdater, + IPostDeleter $postDeleter ) { parent::__construct( $app, $settings, $sessionManager ); - if( $postRepository === null ) - { - throw new \InvalidArgumentException( 'IPostRepository must be injected' ); - } $this->_postRepository = $postRepository; - - if( $categoryRepository === null ) - { - throw new \InvalidArgumentException( 'ICategoryRepository must be injected' ); - } $this->_categoryRepository = $categoryRepository; - - if( $tagRepository === null ) - { - throw new \InvalidArgumentException( 'ITagRepository must be injected' ); - } $this->_tagRepository = $tagRepository; - - if( $postCreator === null ) - { - throw new \InvalidArgumentException( 'IPostCreator must be injected' ); - } $this->_postCreator = $postCreator; - - if( $postUpdater === null ) - { - throw new \InvalidArgumentException( 'IPostUpdater must be injected' ); - } $this->_postUpdater = $postUpdater; - - if( $postDeleter === null ) - { - throw new \InvalidArgumentException( 'IPostDeleter must be injected' ); - } $this->_postDeleter = $postDeleter; } diff --git a/src/Cms/Controllers/Admin/Profile.php b/src/Cms/Controllers/Admin/Profile.php index 90dd72a..65c8a54 100644 --- a/src/Cms/Controllers/Admin/Profile.php +++ b/src/Cms/Controllers/Admin/Profile.php @@ -32,38 +32,24 @@ class Profile extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IUserRepository|null $repository - * @param PasswordHasher|null $hasher - * @param IUserUpdater|null $userUpdater + * @param IUserRepository $repository + * @param PasswordHasher $hasher + * @param IUserUpdater $userUpdater * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IUserRepository $repository = null, - ?PasswordHasher $hasher = null, - ?IUserUpdater $userUpdater = null + IUserRepository $repository, + PasswordHasher $hasher, + IUserUpdater $userUpdater ) { parent::__construct( $app, $settings, $sessionManager ); - if( $repository === null ) - { - throw new \InvalidArgumentException( 'IUserRepository must be injected' ); - } $this->_repository = $repository; - - if( $hasher === null ) - { - throw new \InvalidArgumentException( 'PasswordHasher must be injected' ); - } $this->_hasher = $hasher; - - if( $userUpdater === null ) - { - throw new \InvalidArgumentException( 'IUserUpdater must be injected' ); - } $this->_userUpdater = $userUpdater; } diff --git a/src/Cms/Controllers/Admin/Tags.php b/src/Cms/Controllers/Admin/Tags.php index 24d34f0..9a09d4b 100644 --- a/src/Cms/Controllers/Admin/Tags.php +++ b/src/Cms/Controllers/Admin/Tags.php @@ -33,30 +33,21 @@ class Tags extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param ITagRepository|null $tagRepository - * @param SlugGenerator|null $slugGenerator + * @param ITagRepository $tagRepository + * @param SlugGenerator $slugGenerator * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?ITagRepository $tagRepository = null, - ?SlugGenerator $slugGenerator = null + ITagRepository $tagRepository, + SlugGenerator $slugGenerator ) { parent::__construct( $app, $settings, $sessionManager ); - if( $tagRepository === null ) - { - throw new \InvalidArgumentException( 'ITagRepository must be injected' ); - } $this->_tagRepository = $tagRepository; - - if( $slugGenerator === null ) - { - throw new \InvalidArgumentException( 'SlugGenerator must be injected' ); - } $this->_slugGenerator = $slugGenerator; } diff --git a/src/Cms/Controllers/Admin/Users.php b/src/Cms/Controllers/Admin/Users.php index f285565..bed5ac7 100644 --- a/src/Cms/Controllers/Admin/Users.php +++ b/src/Cms/Controllers/Admin/Users.php @@ -36,45 +36,26 @@ class Users extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IUserRepository|null $repository - * @param IUserCreator|null $userCreator - * @param IUserUpdater|null $userUpdater - * @param IUserDeleter|null $userDeleter + * @param IUserRepository $repository + * @param IUserCreator $userCreator + * @param IUserUpdater $userUpdater + * @param IUserDeleter $userDeleter */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IUserRepository $repository = null, - ?IUserCreator $userCreator = null, - ?IUserUpdater $userUpdater = null, - ?IUserDeleter $userDeleter = null + IUserRepository $repository, + IUserCreator $userCreator, + IUserUpdater $userUpdater, + IUserDeleter $userDeleter ) { parent::__construct( $app, $settings, $sessionManager ); - if( $repository === null ) - { - throw new \InvalidArgumentException( 'IUserRepository must be injected' ); - } $this->_repository = $repository; - - if( $userCreator === null ) - { - throw new \InvalidArgumentException( 'IUserCreator must be injected' ); - } $this->_userCreator = $userCreator; - - if( $userUpdater === null ) - { - throw new \InvalidArgumentException( 'IUserUpdater must be injected' ); - } $this->_userUpdater = $userUpdater; - - if( $userDeleter === null ) - { - throw new \InvalidArgumentException( 'IUserDeleter must be injected' ); - } $this->_userDeleter = $userDeleter; } diff --git a/src/Cms/Controllers/Auth/Login.php b/src/Cms/Controllers/Auth/Login.php index e680679..949e931 100644 --- a/src/Cms/Controllers/Auth/Login.php +++ b/src/Cms/Controllers/Auth/Login.php @@ -102,7 +102,7 @@ public function login( Request $request ): never } // Attempt authentication - if( !$this->_authentication->attempt( $dto->username, $dto->password, $dto->remember ) ) + if( !$this->_authentication->attempt( $dto->username, $dto->password, $dto->remember ?? false ) ) { $this->redirect( 'login', [], [FlashMessageType::ERROR->value, 'Invalid username or password.'] ); } diff --git a/src/Cms/Controllers/Auth/PasswordReset.php b/src/Cms/Controllers/Auth/PasswordReset.php index fbb81f5..de8ddbd 100644 --- a/src/Cms/Controllers/Auth/PasswordReset.php +++ b/src/Cms/Controllers/Auth/PasswordReset.php @@ -32,22 +32,16 @@ class PasswordReset extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IPasswordResetter|null $passwordResetter - * @throws \Exception + * @param IPasswordResetter $passwordResetter */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IPasswordResetter $passwordResetter = null + IPasswordResetter $passwordResetter ) { parent::__construct( $app, $settings, $sessionManager ); - - if( $passwordResetter === null ) - { - throw new \InvalidArgumentException( 'IPasswordResetter must be injected' ); - } $this->_passwordResetter = $passwordResetter; } diff --git a/src/Cms/Controllers/Blog.php b/src/Cms/Controllers/Blog.php index 03e3035..419ac30 100644 --- a/src/Cms/Controllers/Blog.php +++ b/src/Cms/Controllers/Blog.php @@ -33,43 +33,26 @@ class Blog extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IPostRepository|null $postRepository - * @param ICategoryRepository|null $categoryRepository - * @param ITagRepository|null $tagRepository - * @param IUserRepository|null $userRepository - * @param EditorJsRenderer|null $renderer + * @param IPostRepository $postRepository + * @param ICategoryRepository $categoryRepository + * @param ITagRepository $tagRepository + * @param IUserRepository $userRepository + * @param EditorJsRenderer $renderer * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IPostRepository $postRepository = null, - ?ICategoryRepository $categoryRepository = null, - ?ITagRepository $tagRepository = null, - ?IUserRepository $userRepository = null, - ?EditorJsRenderer $renderer = null + IPostRepository $postRepository, + ICategoryRepository $categoryRepository, + ITagRepository $tagRepository, + IUserRepository $userRepository, + EditorJsRenderer $renderer ) { parent::__construct( $app, $settings, $sessionManager ); - // Pure dependency injection - no service locator fallback - if( $postRepository === null ) { - throw new \InvalidArgumentException( 'IPostRepository must be injected' ); - } - if( $categoryRepository === null ) { - throw new \InvalidArgumentException( 'ICategoryRepository must be injected' ); - } - if( $tagRepository === null ) { - throw new \InvalidArgumentException( 'ITagRepository must be injected' ); - } - if( $userRepository === null ) { - throw new \InvalidArgumentException( 'IUserRepository must be injected' ); - } - if( $renderer === null ) { - throw new \InvalidArgumentException( 'EditorJsRenderer must be injected' ); - } - $this->_postRepository = $postRepository; $this->_categoryRepository = $categoryRepository; $this->_tagRepository = $tagRepository; @@ -119,20 +102,20 @@ public function show( Request $request ): string $slug = $request->getRouteParameter( 'slug', '' ); $post = $this->_postRepository->findBySlug( $slug ); - if( !$post ) + $responseStatus = HttpResponseStatus::OK; + + if( !$post || !$post->isPublished() ) { $post = new Post(); $post->setTitle( 'Article Not Found' ); $post->setBody( 'The requested article does not exist.' ); $post->setSlug( $slug ); + $responseStatus = HttpResponseStatus::NOT_FOUND; } else { // Increment view count for published posts - if( $post->isPublished() ) - { - $this->_postRepository->incrementViewCount( $post->getId() ); - } + $this->_postRepository->incrementViewCount( $post->getId() ); } $categories = $this->_categoryRepository->all(); @@ -142,8 +125,14 @@ public function show( Request $request ): string $content = $post->getContent(); $renderedContent = $this->_renderer?->render( $content ) ?? (is_array($content) ? json_encode($content) : $content); + // Fallback to plain text body if Editor.js content is empty + if( empty( trim( $renderedContent ) ) && !empty( $post->getBody() ) ) + { + $renderedContent = '
' . htmlspecialchars( $post->getBody() ) . '
'; + } + return $this->renderHtml( - HttpResponseStatus::OK, + $responseStatus, [ 'Categories' => $categories, 'Tags' => $tags, @@ -165,7 +154,7 @@ public function show( Request $request ): string #[Get('/author/:username', name: 'blog_author')] public function author( Request $request ): string { - $authorName = $request->getRouteParameter( 'author', '' ); + $authorName = $request->getRouteParameter( 'username', '' ); // Look up user by username $user = $this->_userRepository->findByUsername( $authorName ); @@ -204,7 +193,7 @@ public function author( Request $request ): string #[Get('/tag/:slug', name: 'blog_tag')] public function tag( Request $request ): string { - $tagSlug = $request->getRouteParameter( 'tag', '' ); + $tagSlug = $request->getRouteParameter( 'slug', '' ); $tag = $this->_tagRepository->findBySlug( $tagSlug ); if( !$tag ) @@ -245,7 +234,7 @@ public function tag( Request $request ): string #[Get('/category/:slug', name: 'blog_category')] public function category( Request $request ): string { - $categorySlug = $request->getRouteParameter( 'category', '' ); + $categorySlug = $request->getRouteParameter( 'slug', '' ); $category = $this->_categoryRepository->findBySlug( $categorySlug ); if( !$category ) diff --git a/src/Cms/Controllers/Calendar.php b/src/Cms/Controllers/Calendar.php index 0947928..05b6e11 100644 --- a/src/Cms/Controllers/Calendar.php +++ b/src/Cms/Controllers/Calendar.php @@ -30,30 +30,21 @@ class Calendar extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IEventRepository|null $eventRepository - * @param IEventCategoryRepository|null $categoryRepository + * @param IEventRepository $eventRepository + * @param IEventCategoryRepository $categoryRepository * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IEventRepository $eventRepository = null, - ?IEventCategoryRepository $categoryRepository = null + IEventRepository $eventRepository, + IEventCategoryRepository $categoryRepository ) { parent::__construct( $app, $settings, $sessionManager ); - if( $eventRepository === null ) - { - throw new \InvalidArgumentException( 'IEventRepository must be injected' ); - } $this->_eventRepository = $eventRepository; - - if( $categoryRepository === null ) - { - throw new \InvalidArgumentException( 'IEventCategoryRepository must be injected' ); - } $this->_categoryRepository = $categoryRepository; } diff --git a/src/Cms/Controllers/Member/Profile.php b/src/Cms/Controllers/Member/Profile.php index 6e3c0d0..78e4395 100644 --- a/src/Cms/Controllers/Member/Profile.php +++ b/src/Cms/Controllers/Member/Profile.php @@ -31,38 +31,24 @@ class Profile extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IUserRepository|null $repository - * @param PasswordHasher|null $hasher - * @param IUserUpdater|null $userUpdater + * @param IUserRepository $repository + * @param PasswordHasher $hasher + * @param IUserUpdater $userUpdater * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IUserRepository $repository = null, - ?PasswordHasher $hasher = null, - ?IUserUpdater $userUpdater = null + IUserRepository $repository, + PasswordHasher $hasher, + IUserUpdater $userUpdater ) { parent::__construct( $app, $settings, $sessionManager ); - if( $repository === null ) - { - throw new \InvalidArgumentException( 'IUserRepository must be injected' ); - } $this->_repository = $repository; - - if( $hasher === null ) - { - throw new \InvalidArgumentException( 'PasswordHasher must be injected' ); - } $this->_hasher = $hasher; - - if( $userUpdater === null ) - { - throw new \InvalidArgumentException( 'IUserUpdater must be injected' ); - } $this->_userUpdater = $userUpdater; } diff --git a/src/Cms/Controllers/Member/Registration.php b/src/Cms/Controllers/Member/Registration.php index 083d8f6..23051f7 100644 --- a/src/Cms/Controllers/Member/Registration.php +++ b/src/Cms/Controllers/Member/Registration.php @@ -36,46 +36,27 @@ class Registration extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IRegistrationService|null $registrationService - * @param IEmailVerifier|null $emailVerifier - * @param ResendVerificationThrottle|null $resendThrottle - * @param IIpResolver|null $ipResolver + * @param IRegistrationService $registrationService + * @param IEmailVerifier $emailVerifier + * @param ResendVerificationThrottle $resendThrottle + * @param IIpResolver $ipResolver * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IRegistrationService $registrationService = null, - ?IEmailVerifier $emailVerifier = null, - ?ResendVerificationThrottle $resendThrottle = null, - ?IIpResolver $ipResolver = null + IRegistrationService $registrationService, + IEmailVerifier $emailVerifier, + ResendVerificationThrottle $resendThrottle, + IIpResolver $ipResolver ) { parent::__construct( $app, $settings, $sessionManager ); - if( $registrationService === null ) - { - throw new \InvalidArgumentException( 'IRegistrationService must be injected' ); - } $this->_registrationService = $registrationService; - - if( $emailVerifier === null ) - { - throw new \InvalidArgumentException( 'IEmailVerifier must be injected' ); - } $this->_emailVerifier = $emailVerifier; - - if( $resendThrottle === null ) - { - throw new \InvalidArgumentException( 'ResendVerificationThrottle must be injected' ); - } $this->_resendThrottle = $resendThrottle; - - if( $ipResolver === null ) - { - throw new \InvalidArgumentException( 'IIpResolver must be injected' ); - } $this->_ipResolver = $ipResolver; } diff --git a/src/Cms/Controllers/Pages.php b/src/Cms/Controllers/Pages.php index 68b4818..50ffd4f 100644 --- a/src/Cms/Controllers/Pages.php +++ b/src/Cms/Controllers/Pages.php @@ -34,31 +34,20 @@ class Pages extends Content * @param IMvcApplication $app * @param SettingManager $settings * @param SessionManager $sessionManager - * @param IPageRepository|null $pageRepository - * @param EditorJsRenderer|null $renderer + * @param IPageRepository $pageRepository + * @param EditorJsRenderer $renderer * @throws \Exception */ public function __construct( IMvcApplication $app, SettingManager $settings, SessionManager $sessionManager, - ?IPageRepository $pageRepository = null, - ?EditorJsRenderer $renderer = null + IPageRepository $pageRepository, + EditorJsRenderer $renderer ) { parent::__construct( $app, $settings, $sessionManager ); - // Pure dependency injection - no service locator fallback - if( $pageRepository === null ) - { - throw new \InvalidArgumentException( 'IPageRepository must be injected' ); - } - - if( $renderer === null ) - { - throw new \InvalidArgumentException( 'EditorJsRenderer must be injected' ); - } - $this->_pageRepository = $pageRepository; $this->_renderer = $renderer; } diff --git a/tests/Unit/BootstrapTest.php b/tests/Unit/BootstrapTest.php index 53c36a0..e43edf1 100644 --- a/tests/Unit/BootstrapTest.php +++ b/tests/Unit/BootstrapTest.php @@ -176,42 +176,55 @@ public function testBootDelegatesToMvc() } /** - * Test that CMS Boot registers bubble exceptions in Registry + * Test that CMS Boot loads passthrough exceptions from YAML config */ - public function testBootRegistersBubbleExceptions() + public function testBootLoadsPassthroughExceptionsFromConfig() { - // Clear registry before test - Registry::getInstance()->set( 'BubbleExceptions', null ); - - // Boot the CMS application - $app = Boot( 'examples/config' ); - - // Verify bubble exceptions are registered - $bubbleExceptions = Registry::getInstance()->get( 'BubbleExceptions' ); - - $this->assertIsArray( $bubbleExceptions ); - $this->assertContains( 'Neuron\\Cms\\Exceptions\\UnauthenticatedException', $bubbleExceptions ); - $this->assertContains( 'Neuron\\Cms\\Exceptions\\EmailVerificationRequiredException', $bubbleExceptions ); - $this->assertContains( 'Neuron\\Cms\\Exceptions\\CsrfValidationException', $bubbleExceptions ); + // Boot a fresh application in isolation to avoid Registry state pollution from previous tests + // Use a separate PHP process to ensure clean state + $code = <<<'PHP' + require 'vendor/autoload.php'; + $app = Neuron\Cms\boot('examples/config'); + $exceptions = Neuron\Patterns\Registry::getInstance()->get('PassthroughExceptions'); + echo json_encode($exceptions); + PHP; + + $output = shell_exec("php -r " . escapeshellarg($code)); + $passthroughExceptions = json_decode($output, true); + + $this->assertIsArray( $passthroughExceptions, 'PassthroughExceptions should be an array' ); + $this->assertNotEmpty( $passthroughExceptions, 'PassthroughExceptions should not be empty. Got: ' . json_encode( $passthroughExceptions ) ); + + // These are defined in examples/config/neuron.yaml + $this->assertContains( 'Neuron\\Cms\\Exceptions\\UnauthenticatedException', $passthroughExceptions ); + $this->assertContains( 'Neuron\\Cms\\Exceptions\\EmailVerificationRequiredException', $passthroughExceptions ); + $this->assertContains( 'Neuron\\Cms\\Exceptions\\CsrfValidationException', $passthroughExceptions ); } /** - * Test that registered bubble exceptions are the correct classes + * Test that passthrough exceptions loaded from config are valid classes */ - public function testBubbleExceptionsAreValidClasses() + public function testPassthroughExceptionsAreValidClasses() { - // Boot the CMS application - $app = Boot( 'examples/config' ); + // Boot a fresh application in isolation + $code = <<<'PHP' + require 'vendor/autoload.php'; + $app = Neuron\Cms\boot('examples/config'); + $exceptions = Neuron\Patterns\Registry::getInstance()->get('PassthroughExceptions'); + echo json_encode($exceptions); + PHP; + + $output = shell_exec("php -r " . escapeshellarg($code)); + $passthroughExceptions = json_decode($output, true); - // Get registered exceptions - $bubbleExceptions = Registry::getInstance()->get( 'BubbleExceptions' ); + $this->assertIsArray( $passthroughExceptions ); // Verify each exception class exists - foreach( $bubbleExceptions as $exceptionClass ) + foreach( $passthroughExceptions as $exceptionClass ) { $this->assertTrue( class_exists( $exceptionClass ), - "Bubble exception class does not exist: $exceptionClass" + "Passthrough exception class does not exist: $exceptionClass" ); } } diff --git a/tests/Unit/Cms/Auth/CsrfFilterTest.php b/tests/Unit/Cms/Auth/CsrfFilterTest.php index c7e6e7c..6a7e332 100644 --- a/tests/Unit/Cms/Auth/CsrfFilterTest.php +++ b/tests/Unit/Cms/Auth/CsrfFilterTest.php @@ -263,20 +263,19 @@ public function testPostTokenExtractionLimitation(): void $method->setAccessible( true ); $token = $method->invoke( $this->_filter ); - // In unit tests, filter_input(INPUT_POST) returns null because - // it reads from PHP's input buffer, not $_POST. Therefore, the - // method falls back to the header token even though we set POST. + // POST token should take precedence over header token. + // The implementation uses Neuron\Data\Filters\Post which can read $_POST + // directly, so it works correctly in both unit tests and real requests. $this->assertEquals( - 'header-token', + 'post-token', $token, - 'Unit tests fall back to header because filter_input(INPUT_POST) cannot read $_POST' + 'POST token takes precedence over header token' ); // Verify our test setup was correct (POST was set) $this->assertEquals( 'post-token', $_POST['csrf_token'] ); - // In real requests (integration/E2E tests), POST would take precedence - // and the token would be 'post-token', not 'header-token' + // This behavior is now consistent in both unit tests and real requests // Cleanup unset( $_POST['csrf_token'] ); diff --git a/tests/Unit/Cms/BlogControllerTest.php b/tests/Unit/Cms/BlogControllerTest.php index ad00b81..bef0e21 100644 --- a/tests/Unit/Cms/BlogControllerTest.php +++ b/tests/Unit/Cms/BlogControllerTest.php @@ -386,7 +386,7 @@ public function testTagFiltersPostsByTag(): void $blog = $this->createBlogWithInjectedRepositories(); $request = new Request(); - $request->setRouteParameters( [ 'tag' => $tag->getSlug() ] ); + $request->setRouteParameters( [ 'slug' => $tag->getSlug() ] ); $result = $blog->tag( $request ); $this->assertIsString( $result ); @@ -404,7 +404,7 @@ public function testCategoryFiltersPostsByCategory(): void $blog = $this->createBlogWithInjectedRepositories(); $request = new Request(); - $request->setRouteParameters( [ 'category' => $category->getSlug() ] ); + $request->setRouteParameters( [ 'slug' => $category->getSlug() ] ); $result = $blog->category( $request ); $this->assertIsString( $result ); @@ -422,11 +422,11 @@ public function testAuthorFiltersPostsByAuthor(): void $blog = $this->createBlogWithInjectedRepositories(); - // Create mock request with author parameter + // Create mock request with username parameter $request = $this->createMock( Request::class ); $request->expects( $this->once() ) ->method( 'getRouteParameter' ) - ->with( 'author', '' ) + ->with( 'username', '' ) ->willReturn( 'author1' ); $result = $blog->author( $request ); diff --git a/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php b/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php index b27d32f..dbb6abd 100644 --- a/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php +++ b/tests/Unit/Cms/Controllers/Admin/CategoriesTest.php @@ -84,7 +84,7 @@ public function testConstructorThrowsExceptionWithoutSettingManager(): void public function testConstructorThrowsExceptionWithoutCategoryRepository(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); $mockSettings = Registry::getInstance()->get( 'Settings' ); $mockSessionManager = $this->createMock( SessionManager::class ); diff --git a/tests/Unit/Cms/Controllers/Admin/PostsTest.php b/tests/Unit/Cms/Controllers/Admin/PostsTest.php index c0706bb..229cf9b 100644 --- a/tests/Unit/Cms/Controllers/Admin/PostsTest.php +++ b/tests/Unit/Cms/Controllers/Admin/PostsTest.php @@ -85,7 +85,7 @@ public function testConstructorThrowsExceptionWithoutSettingManager(): void public function testConstructorThrowsExceptionWithoutPostRepository(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); $mockSettings = Registry::getInstance()->get( 'Settings' ); $mockSessionManager = $this->createMock( SessionManager::class ); diff --git a/tests/Unit/Cms/Controllers/Admin/UsersTest.php b/tests/Unit/Cms/Controllers/Admin/UsersTest.php index 75b0b30..ec6f481 100644 --- a/tests/Unit/Cms/Controllers/Admin/UsersTest.php +++ b/tests/Unit/Cms/Controllers/Admin/UsersTest.php @@ -78,7 +78,7 @@ public function testConstructorThrowsExceptionWithoutSettingManager(): void public function testConstructorThrowsExceptionWithoutUserRepository(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); $mockSettings = Registry::getInstance()->get( 'Settings' ); $mockSessionManager = $this->createMock( SessionManager::class ); diff --git a/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php b/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php index 4aa086d..b1a5ffd 100644 --- a/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php +++ b/tests/Unit/Cms/Controllers/Auth/PasswordResetTest.php @@ -111,7 +111,7 @@ public function testConstructorWithDependencies(): void public function testConstructorThrowsExceptionWithoutPasswordResetter(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); $mockSettingManager = Registry::getInstance()->get( 'Settings' ); $mockSessionManager = $this->createMock( \Neuron\Cms\Auth\SessionManager::class ); diff --git a/tests/Unit/Cms/Controllers/Member/RegistrationTest.php b/tests/Unit/Cms/Controllers/Member/RegistrationTest.php index 32afdc8..6685d0d 100644 --- a/tests/Unit/Cms/Controllers/Member/RegistrationTest.php +++ b/tests/Unit/Cms/Controllers/Member/RegistrationTest.php @@ -126,7 +126,7 @@ public function testConstructorWithAllDependencies(): void public function testConstructorThrowsExceptionWithoutRegistrationService(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); new Registration( $this->mockApp, @@ -141,7 +141,7 @@ public function testConstructorThrowsExceptionWithoutRegistrationService(): void public function testConstructorThrowsExceptionWithoutEmailVerifier(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); new Registration( $this->mockApp, @@ -156,7 +156,7 @@ public function testConstructorThrowsExceptionWithoutEmailVerifier(): void public function testConstructorThrowsExceptionWithoutResendThrottle(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); new Registration( $this->mockApp, @@ -171,7 +171,7 @@ public function testConstructorThrowsExceptionWithoutResendThrottle(): void public function testConstructorThrowsExceptionWithoutIpResolver(): void { - $this->expectException( \InvalidArgumentException::class ); + $this->expectException( \TypeError::class ); new Registration( $this->mockApp,