├── CHANGELOG.md ├── CONTRIBUTING.md ├── Classes ├── Controller │ ├── AbstractController.php │ ├── BackendController.php │ ├── BlogController.php │ ├── CommentController.php │ └── PostController.php ├── Domain │ ├── Model │ │ ├── Administrator.php │ │ ├── Blog.php │ │ ├── Comment.php │ │ ├── FrontendUser.php │ │ ├── FrontendUserGroup.php │ │ ├── Info.php │ │ ├── Person.php │ │ ├── Post.php │ │ └── Tag.php │ ├── Repository │ │ ├── AdministratorRepository.php │ │ ├── BlogRepository.php │ │ ├── CommentRepository.php │ │ ├── PersonRepository.php │ │ └── PostRepository.php │ └── Validator │ │ ├── BlogValidator.php │ │ ├── PostValidator.php │ │ └── TitleValidator.php ├── Exception │ └── NoBlogAdminAccessException.php ├── ExpressionLanguage │ └── ExtensionConfigurationProvider.php ├── PageTitle │ └── BlogPageTitleProvider.php ├── Property │ └── TypeConverters │ │ └── HiddenCommentConverter.php ├── Service │ ├── BlogFactory.php │ ├── BlogValidationService.php │ └── PostValidationService.php ├── Upgrades │ ├── MigratePluginsToContentElementsUpgradeWizard.php │ └── PluginUpgradeWizard.php └── ViewHelpers │ └── GravatarViewHelper.php ├── Configuration ├── Backend │ └── Modules.php ├── ExpressionLanguage.php ├── Extbase │ └── Persistence │ │ └── Classes.php ├── FlexForms │ └── PluginSettings.xml ├── Icons.php ├── Services.yaml ├── Sets │ ├── BlogExample │ │ ├── config.yaml │ │ ├── settings.definitions.yaml │ │ └── setup.typoscript │ ├── DefaultStyles │ │ ├── config.yaml │ │ └── setup.typoscript │ └── RssFeed │ │ ├── config.yaml │ │ ├── constants.typoscript │ │ ├── settings.definitions.yaml │ │ └── setup.typoscript ├── TCA │ ├── Overrides │ │ ├── fe_users.php │ │ └── tt_content.php │ ├── tx_blogexample_domain_model_blog.php │ ├── tx_blogexample_domain_model_comment.php │ ├── tx_blogexample_domain_model_info.php │ ├── tx_blogexample_domain_model_person.php │ ├── tx_blogexample_domain_model_post.php │ └── tx_blogexample_domain_model_tag.php └── page.tsconfig ├── Makefile ├── README.md ├── Resources ├── Private │ ├── Backend │ │ └── Templates │ │ │ ├── Index.html │ │ │ ├── ShowAllComments.html │ │ │ ├── ShowBlog.html │ │ │ └── ShowPost.html │ ├── Language │ │ ├── Module │ │ │ └── locallang_mod.xlf │ │ ├── de.locallang.xlf │ │ ├── locallang.xlf │ │ └── locallang_db.xlf │ ├── Layouts │ │ └── Default.html │ ├── Partials │ │ ├── BlogForm.html │ │ ├── CommentForm.html │ │ ├── FormErrors.html │ │ ├── Pagination.html │ │ ├── PostForm.html │ │ ├── PostMetaData.html │ │ └── PostTags.html │ └── Templates │ │ ├── Blog │ │ ├── Edit.html │ │ ├── Index.html │ │ └── New.html │ │ └── Post │ │ ├── DisplayRssList.html │ │ ├── Edit.html │ │ ├── Index.html │ │ ├── Index.txt │ │ ├── New.html │ │ └── Show.html └── Public │ ├── Css │ └── BlogExample.css │ └── Icons │ ├── Extension.svg │ ├── FlashMessages │ ├── error.png │ ├── information.png │ ├── notice.png │ ├── ok.png │ └── warning.png │ ├── default_gravatar.gif │ ├── icon_close.gif │ ├── icon_delete.gif │ ├── icon_edit.gif │ ├── icon_new.gif │ ├── icon_next.gif │ ├── icon_plaintext.gif │ ├── icon_populate.gif │ ├── icon_previous.gif │ ├── icon_relation.gif │ ├── icon_tx_blogexample_domain_model_blog.gif │ ├── icon_tx_blogexample_domain_model_comment.gif │ ├── icon_tx_blogexample_domain_model_person.gif │ ├── icon_tx_blogexample_domain_model_post.gif │ ├── icon_tx_blogexample_domain_model_tag.gif │ └── module-blog.svg ├── composer.json ├── ext_conf_template.txt ├── ext_emconf.php ├── ext_localconf.php └── ext_tables.sql /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](https://semver.org/). 5 | 6 | 7 | ## Unreleased 8 | 9 | ### Fixed 10 | - Add return type to `ModuleController->initializeAction()` (#74) 11 | 12 | ## 12.1.0 - 2024-04-15 13 | 14 | ### Added 15 | - Add page title provider for blog post indexAction and showAction (#43) 16 | 17 | ### Fixed 18 | - Update locallang_db.xlf - missing field names added (#76) 19 | - Correct the integration of categories (#77) 20 | - Invoke addFlashMessage() correctly when deleting a post or a blog (#83, #86) 21 | 22 | ## 12.0.4 - 2023-09-16 23 | 24 | ### Fixed 25 | - Remove superfluous CSH language file (#57) 26 | - Streamline XLIFF files (#58) 27 | 28 | ## 12.0.3 - 2023-08-07 29 | 30 | ### Fixed 31 | - Substitute ExtensionManagementUtility::getFileFieldTCAConfig() with TCA file type (#48) 32 | 33 | ## 12.0.2 - 2022-10-09 34 | 35 | ### Changed 36 | - The main branch was renamed from `master` to `main` 37 | 38 | ### Fixed 39 | - Fix `Services.yaml` for type converter example (#41) 40 | 41 | ## 12.0.1 - 2022-10-07 42 | 43 | ### Added 44 | - Add TypeConverter examples (#34) 45 | 46 | ### Changed 47 | - Changed the vendor to t3docs (#40) 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to this extension 2 | 3 | For help about contributing to the documentation in general, see https://docs.typo3.org/typo3cms/HowToDocument/ 4 | 5 | You are welcome to contribute to this example extension. Please note that the code is used in different manuals 6 | to demonstrate certain features of TYPO3 and Extbase. 7 | 8 | Talk to us on Slack, channel #typo3-documentation or open an [issue](https://github.com/TYPO3-Documentation/blog_example/issues) to be sure 9 | that your intended changes are fitting the didactics of these manuals. 10 | 11 | ## Run tests and apply coding guidelines 12 | 13 | The blog-example comes with a simple demo set of tests. It relies 14 | on the runTests.sh script which is a simplified version of a similar script from the TYPO3 core. 15 | Find detailed usage examples by executing `Build/Scripts/runTests.sh -h` and have a look at 16 | `.github/workflows/tests.yml` to see how this is used in CI. 17 | 18 | Install the requirements: 19 | 20 | ``` 21 | make install 22 | ``` 23 | 24 | Apply rector and automatic coding guideline fixes: 25 | 26 | ``` 27 | make fix 28 | ``` 29 | 30 | Run tests: 31 | 32 | ``` 33 | make test 34 | ``` 35 | 36 | See help: 37 | 38 | ``` 39 | make 40 | ``` 41 | 42 | If you have no `make` installed or which for finer control, you can run the tests directly: 43 | 44 | ``` 45 | Build/Scripts/runTests.sh -s h 46 | ``` 47 | 48 | # General TYPO3 Support 49 | 50 | If you have some general TYPO3 support questions or need help with TYPO3, please see https://typo3.org/help. 51 | -------------------------------------------------------------------------------- /Classes/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | request->getControllerName(), 39 | $this->actionMethodName, 40 | ); 41 | return $this->translate($locallangKey, $defaultFlashMessage); 42 | } 43 | 44 | protected function hasBlogAdminAccess(): bool 45 | { 46 | // TODO access protection 47 | return true; 48 | } 49 | 50 | /** 51 | * @throws NoBlogAdminAccessException 52 | */ 53 | protected function checkBlogAdminAccess(): void 54 | { 55 | if (!$this->hasBlogAdminAccess()) { 56 | throw new NoBlogAdminAccessException(); 57 | } 58 | } 59 | 60 | /** 61 | * helper function to render localized flashmessages 62 | */ 63 | public function addLocalizedFlashMessage( 64 | string $action, 65 | ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK, 66 | ): void { 67 | $messageLocallangKey = sprintf( 68 | 'flashmessage.%s.%s', 69 | $this->request->getControllerName(), 70 | $action, 71 | ); 72 | $localizedMessage = $this->translate( 73 | $messageLocallangKey, 74 | '[' . $messageLocallangKey . ']', 75 | ); 76 | $titleLocallangKey = sprintf('%s.title', $messageLocallangKey); 77 | $localizedTitle = $this->translate( 78 | $titleLocallangKey, 79 | '[' . $titleLocallangKey . ']', 80 | ); 81 | $this->addFlashMessage($localizedMessage, $localizedTitle, $severity); 82 | } 83 | 84 | /** 85 | * helper function to use localized strings in BlogExample controllers 86 | */ 87 | protected function translate( 88 | string $key, 89 | string $defaultMessage = '', 90 | ): string { 91 | $message = LocalizationUtility::translate($key, 'BlogExample'); 92 | if ($message === null) { 93 | $message = $defaultMessage; 94 | } 95 | return $message; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Classes/Controller/BlogController.php: -------------------------------------------------------------------------------- 1 | blogRepository->findAll(); 54 | $paginator = new QueryResultPaginator( 55 | $allAvailableBlogs, 56 | $currentPage, 57 | (int)($this->settings['itemsPerPage'] ?? 3), 58 | ); 59 | $pagination = new SimplePagination($paginator); 60 | $this->view->assignMultiple([ 61 | 'blogs' => $allAvailableBlogs, 62 | 'paginator' => $paginator, 63 | 'pagination' => $pagination, 64 | 'pages' => range(1, $pagination->getLastPageNumber()), 65 | ]); 66 | return $this->htmlResponse(); 67 | } 68 | 69 | /** 70 | * Output

Hello World!

71 | */ 72 | public function helloWorldAction(): ResponseInterface 73 | { 74 | return $this->htmlResponse('

Hello World!

'); 75 | } 76 | 77 | /** 78 | * Displays a form for creating a new blog 79 | */ 80 | #[IgnoreValidation(['value' => 'newBlog'])] 81 | public function newAction(?Blog $newBlog = null): ResponseInterface 82 | { 83 | $this->view->assignMultiple([ 84 | 'newBlog' => $newBlog, 85 | 'administrators' => $this->administratorRepository->findAll(), 86 | ]); 87 | return $this->htmlResponse(); 88 | } 89 | 90 | /** 91 | * Creates a new blog 92 | * 93 | * $blog is a fresh Blog object which has not yet been added to the 94 | * repository 95 | */ 96 | #[Validate(['param' => 'newBlog', 'validator' => BlogValidator::class])] 97 | public function createAction(Blog $newBlog): ResponseInterface 98 | { 99 | $this->checkBlogAdminAccess(); 100 | $this->blogRepository->add($newBlog); 101 | $this->addFlashMessage('created'); 102 | return $this->redirect('index'); 103 | } 104 | 105 | /** 106 | * Displays a form for editing an existing blog 107 | * 108 | * $blog might also be a clone of the original blog already containing 109 | * modifications if the edit form has been submitted, contained errors and 110 | * therefore ended up in this action again. 111 | */ 112 | #[IgnoreValidation(['value' => 'blog'])] 113 | public function editAction(Blog $blog): ResponseInterface 114 | { 115 | $this->view->assignMultiple([ 116 | 'blog' => $blog, 117 | 'administrators' => $this->administratorRepository->findAll(), 118 | ]); 119 | return $this->htmlResponse(); 120 | } 121 | 122 | /** 123 | * Updates an existing blog 124 | * 125 | * $blog is a not yet persisted clone of the original blog containing 126 | * the modifications 127 | * 128 | * @throws NoBlogAdminAccessException 129 | */ 130 | #[Validate(['param' => 'blog', 'validator' => BlogValidator::class])] 131 | public function updateAction(Blog $blog): ResponseInterface 132 | { 133 | $this->checkBlogAdminAccess(); 134 | $this->blogRepository->update($blog); 135 | $this->addFlashMessage('updated'); 136 | return $this->redirect('index'); 137 | } 138 | 139 | /** 140 | * Deletes an existing blog 141 | * @throws NoBlogAdminAccessException 142 | */ 143 | public function deleteAction(Blog $blog): ResponseInterface 144 | { 145 | $this->checkBlogAdminAccess(); 146 | $this->blogRepository->remove($blog); 147 | $this->addFlashMessage('The blog has been deleted.', 'deleted', ContextualFeedbackSeverity::INFO); 148 | return $this->redirect('index'); 149 | } 150 | 151 | /** 152 | * Deletes an existing blog 153 | * @throws NoBlogAdminAccessException 154 | */ 155 | public function deleteAllAction(): ResponseInterface 156 | { 157 | $this->checkBlogAdminAccess(); 158 | $this->blogRepository->removeAll(); 159 | return $this->redirect('index'); 160 | } 161 | 162 | /** 163 | * Creates a several new blogs 164 | * @throws NoBlogAdminAccessException 165 | */ 166 | public function populateAction(): ResponseInterface 167 | { 168 | $this->checkBlogAdminAccess(); 169 | $numberOfExistingBlogs = $this->blogRepository->countAll(); 170 | for ($blogNumber = $numberOfExistingBlogs + 1; $blogNumber < ($numberOfExistingBlogs + 5); $blogNumber++) { 171 | $blog = $this->blogFactory->createBlog($blogNumber); 172 | $this->blogRepository->add($blog); 173 | } 174 | $this->addFlashMessage('populated'); 175 | return $this->redirect('index'); 176 | } 177 | 178 | public function showBlogAjaxAction(Blog $blog): ResponseInterface 179 | { 180 | $jsonOutput = json_encode($blog); 181 | return $this->jsonResponse($jsonOutput); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Classes/Controller/CommentController.php: -------------------------------------------------------------------------------- 1 | addComment($newComment); 54 | $this->postRepository->update($post); 55 | $this->addFlashMessage('created'); 56 | return $this->redirect('show', 'Post', null, ['post' => $post]); 57 | } 58 | 59 | /** 60 | * Deletes an existing comment 61 | * @throws NoBlogAdminAccessException 62 | */ 63 | public function deleteAction( 64 | Post $post, 65 | Comment $comment, 66 | ): ResponseInterface { 67 | $this->checkBlogAdminAccess(); 68 | $post->removeComment($comment); 69 | $this->postRepository->update($post); 70 | $this->addFlashMessage('deleted', '', ContextualFeedbackSeverity::INFO); 71 | return $this->redirect('show', 'Post', null, ['post' => $post]); 72 | } 73 | 74 | /** 75 | * @throws NoSuchArgumentException 76 | */ 77 | public function initializePublishAction(): void 78 | { 79 | $this->arguments->getArgument('comment') 80 | ->getPropertyMappingConfiguration() 81 | ->setTypeConverter($this->hiddenCommentConverter); 82 | } 83 | 84 | public function publishAction( 85 | Post $post, 86 | Comment $comment, 87 | ): ResponseInterface { 88 | $this->checkBlogAdminAccess(); 89 | $comment->setHidden(false); 90 | $this->commentRepository->update($comment); 91 | $this->addFlashMessage('published', '', ContextualFeedbackSeverity::INFO); 92 | return $this->redirect('show', 'Post', null, ['post' => $post]); 93 | } 94 | 95 | /** 96 | * Deletes all comments of the given post 97 | * @throws NoBlogAdminAccessException 98 | */ 99 | public function deleteAllAction(Post $post): ResponseInterface 100 | { 101 | $this->checkBlogAdminAccess(); 102 | $post->removeAllComments(); 103 | $this->postRepository->update($post); 104 | $this->addFlashMessage('deletedAll', '', ContextualFeedbackSeverity::INFO); 105 | return $this->redirect( 106 | 'edit', 107 | 'Post', 108 | null, 109 | ['post' => $post, 'blog' => $post->getBlog()], 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Classes/Controller/PostController.php: -------------------------------------------------------------------------------- 1 | $tagString, 65 | ]; 66 | return $this->propertyMapper->convert( 67 | $input, 68 | Tag::class, 69 | ); 70 | } 71 | 72 | /** 73 | * This method demonstrates property mapping to an integer 74 | * @throws Exception 75 | */ 76 | protected function mapIntegerFromString(string $numberString = '42'): int 77 | { 78 | return $output = $this->propertyMapper->convert($numberString, 'integer'); 79 | } 80 | 81 | /** 82 | * Displays a list of posts. If $tag is set only posts matching this tag are shown 83 | */ 84 | public function indexAction( 85 | ?Blog $blog = null, 86 | string $tag = '', 87 | int $currentPage = 1, 88 | ): ResponseInterface { 89 | if ($blog == null) { 90 | return (new ForwardResponse('index')) 91 | ->withControllerName('Blog') 92 | ->withExtensionName('blog_example') 93 | ->withArguments(['currentPage' => $currentPage]); 94 | } 95 | $this->blogPageTitleProvider->setTitle($blog->getTitle()); 96 | if (empty($tag)) { 97 | $posts = $this->postRepository->findBy(['blog' => $blog]); 98 | } else { 99 | $tag = urldecode($tag); 100 | $posts = $this->postRepository->findByTagAndBlog($tag, $blog); 101 | $this->view->assign('tag', $tag); 102 | } 103 | $paginator = new QueryResultPaginator( 104 | $posts, 105 | $currentPage, 106 | (int)($this->settings['itemsPerPage'] ?? 3), 107 | ); 108 | $pagination = new SimplePagination($paginator); 109 | $this->view->assignMultiple([ 110 | 'paginator' => $paginator, 111 | 'pagination' => $pagination, 112 | 'pages' => range(1, $pagination->getLastPageNumber()), 113 | 'blog' => $blog, 114 | 'posts' => $posts, 115 | ]); 116 | return $this->htmlResponse(); 117 | } 118 | 119 | /** 120 | * Displays a list of posts as RSS feed 121 | */ 122 | public function displayRssListAction(): ResponseInterface 123 | { 124 | $defaultBlog = $this->settings['defaultBlog'] ?? 0; 125 | if ($defaultBlog > 0) { 126 | $blog = $this->blogRepository->findByUid((int)$defaultBlog); 127 | } else { 128 | $blog = $this->blogRepository->findAll()->getFirst(); 129 | } 130 | $this->view->assign('blog', $blog); 131 | return $this->responseFactory->createResponse() 132 | ->withHeader('Content-Type', 'text/xml; charset=utf-8') 133 | ->withBody($this->streamFactory->createStream($this->view->render())); 134 | } 135 | 136 | /** 137 | * Displays one single post 138 | */ 139 | #[IgnoreValidation(['value' => 'newComment'])] 140 | public function showAction( 141 | Post $post, 142 | ?Comment $newComment = null, 143 | ): ResponseInterface { 144 | $this->blogPageTitleProvider->setTitle($post->getTitle()); 145 | $this->view->assignMultiple([ 146 | 'post' => $post, 147 | 'newComment' => $newComment, 148 | ]); 149 | return $this->htmlResponse(); 150 | } 151 | 152 | /** 153 | * Displays a form for creating a new post 154 | * 155 | * $newPost is a fresh post object taken as a basis for the rendering 156 | */ 157 | #[IgnoreValidation(['value' => 'newPost'])] 158 | public function newAction( 159 | Blog $blog, 160 | ?Post $newPost = null, 161 | ): ResponseInterface { 162 | $this->view->assignMultiple([ 163 | 'authors' => $this->personRepository->findAll(), 164 | 'blog' => $blog, 165 | 'newPost' => $newPost, 166 | 'remainingPosts' => $this->postRepository->findBy(['blog' => $blog]), 167 | ]); 168 | 169 | return $this->htmlResponse(); 170 | } 171 | 172 | /** 173 | * Creates a new post 174 | * @throws NoBlogAdminAccessException|IllegalObjectTypeException 175 | */ 176 | public function createAction( 177 | Blog $blog, 178 | Post $newPost, 179 | ): ResponseInterface { 180 | $this->checkBlogAdminAccess(); 181 | $blog->addPost($newPost); 182 | $newPost->setBlog($blog); 183 | $this->postRepository->add($newPost); 184 | $this->addFlashMessage('created'); 185 | return $this->redirect('index', null, null, ['blog' => $blog]); 186 | } 187 | 188 | /** 189 | * Displays a form to edit an existing post 190 | */ 191 | #[IgnoreValidation(['value' => 'post'])] 192 | public function editAction(Blog $blog, Post $post): ResponseInterface 193 | { 194 | $this->view->assignMultiple([ 195 | 'authors' => $this->personRepository->findAll(), 196 | 'blog' => $blog, 197 | 'post' => $post, 198 | 'remainingPosts' => $this->postRepository->findRemaining($post), 199 | ]); 200 | return $this->htmlResponse(); 201 | } 202 | 203 | /** 204 | * Updates an existing post 205 | * 206 | * $post is a clone of the original post with the updated values already applied 207 | * 208 | * @throws NoBlogAdminAccessException 209 | */ 210 | public function updateAction( 211 | Blog $blog, 212 | Post $post, 213 | ): ResponseInterface { 214 | $this->checkBlogAdminAccess(); 215 | $this->postRepository->update($post); 216 | $this->addFlashMessage('updated'); 217 | return $this->redirect( 218 | 'show', 219 | null, 220 | null, 221 | ['post' => $post, 'blog' => $blog], 222 | ); 223 | } 224 | 225 | /** 226 | * Deletes an existing post 227 | * @throws NoBlogAdminAccessException 228 | */ 229 | public function deleteAction( 230 | Blog $blog, 231 | Post $post, 232 | ): ResponseInterface { 233 | $this->checkBlogAdminAccess(); 234 | $this->postRepository->remove($post); 235 | $this->addFlashMessage('The post has been deleted.', 'Deleted', ContextualFeedbackSeverity::INFO); 236 | return $this->redirect('index', null, null, ['blog' => $blog]); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Administrator.php: -------------------------------------------------------------------------------- 1 | TitleValidator::class])] 34 | public string $title = ''; 35 | 36 | #[Validate(['validator' => 'StringLength', 'options' => ['minimum' => 5, 'maximum' => 80]])] 37 | public string|null $subtitle = null; 38 | 39 | /** 40 | * A short description of the blog 41 | */ 42 | #[Validate(['validator' => 'StringLength', 'options' => ['maximum' => 150]])] 43 | public string $description = ''; 44 | 45 | /** 46 | * @var ?ObjectStorage 47 | */ 48 | public ?ObjectStorage $logo = null; 49 | 50 | /** 51 | * @var ?ObjectStorage 52 | */ 53 | #[Lazy()] 54 | #[Cascade(['value' => 'remove'])] 55 | public ?ObjectStorage $posts = null; 56 | 57 | /** 58 | * @var ?ObjectStorage 59 | */ 60 | public ?ObjectStorage $categories = null; 61 | 62 | public ?Administrator $administrator = null; 63 | 64 | public function __construct() 65 | { 66 | $this->initializeObject(); 67 | } 68 | 69 | /** 70 | * Initializes all ObjectStorage properties when model is reconstructed from DB (where __construct is not called) 71 | */ 72 | public function initializeObject(): void 73 | { 74 | $this->posts ??= new ObjectStorage(); 75 | $this->categories ??= new ObjectStorage(); 76 | } 77 | 78 | /** 79 | * Adds a post to this blog 80 | */ 81 | public function addPost(Post $post): void 82 | { 83 | $this->posts?->attach($post); 84 | } 85 | 86 | /** 87 | * Remove a post from this blog 88 | */ 89 | public function removePost(Post $postToRemove): void 90 | { 91 | $this->posts?->detach($postToRemove); 92 | } 93 | 94 | /** 95 | * Remove all posts from this blog 96 | */ 97 | public function removeAllPosts(): void 98 | { 99 | $this->posts = new ObjectStorage(); 100 | } 101 | 102 | /** 103 | * Returns all posts in this blog 104 | * 105 | * @return ObjectStorage 106 | */ 107 | public function getPosts(): ObjectStorage 108 | { 109 | return $this->posts; 110 | } 111 | 112 | /** 113 | * @param ObjectStorage $posts 114 | */ 115 | public function setPosts(ObjectStorage $posts): void 116 | { 117 | $this->posts = $posts; 118 | } 119 | 120 | /** 121 | * Add category to a blog 122 | * 123 | * @param Category $category 124 | */ 125 | public function addCategory(Category $category): void 126 | { 127 | $this->categories?->attach($category); 128 | } 129 | 130 | /** 131 | * @param ObjectStorage $categories 132 | */ 133 | public function setCategories(ObjectStorage $categories): void 134 | { 135 | $this->categories = $categories; 136 | } 137 | 138 | /** 139 | * @return ObjectStorage|null 140 | */ 141 | public function getCategories(): ?ObjectStorage 142 | { 143 | return $this->categories; 144 | } 145 | 146 | /** 147 | * Remove category from blog 148 | */ 149 | public function removeCategory(Category $category): void 150 | { 151 | $this->categories?->detach($category); 152 | } 153 | 154 | public function getTitle(): string 155 | { 156 | return $this->title; 157 | } 158 | 159 | public function setTitle(string $title): void 160 | { 161 | $this->title = $title; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Comment.php: -------------------------------------------------------------------------------- 1 | 'NotEmpty'])] 30 | protected string $author = ''; 31 | 32 | #[Validate(['validator' => 'EmailAddress'])] 33 | protected string $email = ''; 34 | 35 | #[Validate(['validator' => 'StringLength', 'options' => ['maximum' => 500]])] 36 | protected string $content = ''; 37 | 38 | protected bool $hidden = true; 39 | 40 | public function __construct() 41 | { 42 | $this->date = new \DateTime(); 43 | } 44 | 45 | /** 46 | * @return \DateTime 47 | */ 48 | public function getDate(): \DateTime 49 | { 50 | return $this->date; 51 | } 52 | 53 | public function setDate(\DateTime $date): void 54 | { 55 | $this->date = $date; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getAuthor(): string 62 | { 63 | return $this->author; 64 | } 65 | 66 | public function setAuthor(string $author): void 67 | { 68 | $this->author = $author; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function getEmail(): string 75 | { 76 | return $this->email; 77 | } 78 | 79 | /** 80 | * @param string $email 81 | * Sets the authors email for this comment 82 | */ 83 | public function setEmail(string $email): void 84 | { 85 | $this->email = $email; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getContent(): string 92 | { 93 | return $this->content; 94 | } 95 | 96 | public function setContent(string $content): void 97 | { 98 | $this->content = $content; 99 | } 100 | 101 | public function isHidden(): bool 102 | { 103 | return $this->hidden; 104 | } 105 | 106 | public function setHidden(bool $hidden): void 107 | { 108 | $this->hidden = $hidden; 109 | } 110 | 111 | /** 112 | * Returns this comment as a formatted string 113 | */ 114 | public function __toString(): string 115 | { 116 | return $this->author . ' (' . $this->email . ') said on ' . $this->date->format('Y-m-d') . ':' . chr(10) . 117 | $this->content . chr(10); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Classes/Domain/Model/FrontendUser.php: -------------------------------------------------------------------------------- 1 | |null 29 | */ 30 | protected ObjectStorage|null $usergroup = null; 31 | public string $name = ''; 32 | public string $email = ''; 33 | public \DateTime|null $lastlogin = null; 34 | 35 | public function __construct() 36 | { 37 | $this->initializeObject(); 38 | } 39 | 40 | /** 41 | * Initializes all ObjectStorage properties when model is reconstructed from DB (where __construct is not called) 42 | */ 43 | public function initializeObject(): void 44 | { 45 | $this->usergroup ??= new ObjectStorage(); 46 | } 47 | 48 | /** 49 | * Sets the usergroups. Keep in mind that the property is called "usergroup" 50 | * although it can hold several usergroups. 51 | * 52 | * @param ObjectStorage $usergroup 53 | */ 54 | public function setUsergroup(ObjectStorage $usergroup): void 55 | { 56 | $this->usergroup = $usergroup; 57 | } 58 | 59 | /** 60 | * Adds a usergroup to the frontend user 61 | */ 62 | public function addUsergroup(FrontendUserGroup $usergroup): void 63 | { 64 | $this->usergroup?->attach($usergroup); 65 | } 66 | 67 | /** 68 | * Removes a usergroup from the frontend user 69 | */ 70 | public function removeUsergroup(FrontendUserGroup $usergroup): void 71 | { 72 | $this->usergroup?->detach($usergroup); 73 | } 74 | 75 | /** 76 | * Returns the usergroups. Keep in mind that the property is called "usergroup" 77 | * although it can hold several usergroups. 78 | * 79 | * @return ObjectStorage An object storage containing the usergroup 80 | */ 81 | public function getUsergroup(): ObjectStorage 82 | { 83 | $this->usergroup ??= new ObjectStorage(); 84 | return $this->usergroup; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Classes/Domain/Model/FrontendUserGroup.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | protected ?ObjectStorage $subgroup = null; 36 | 37 | /** 38 | * Constructs a new Frontend User Group 39 | */ 40 | public function __construct(string $title = '') 41 | { 42 | $this->setTitle($title); 43 | $this->initializeObject(); 44 | } 45 | 46 | /** 47 | * Initializes all ObjectStorage properties when model is reconstructed from DB (where __construct is not called) 48 | */ 49 | public function initializeObject(): void 50 | { 51 | $this->subgroup ??= new ObjectStorage(); 52 | } 53 | 54 | public function getTitle(): string 55 | { 56 | return $this->title; 57 | } 58 | 59 | public function setTitle(string $title): void 60 | { 61 | $this->title = $title; 62 | } 63 | 64 | public function getDescription(): string 65 | { 66 | return $this->description; 67 | } 68 | 69 | public function setDescription(string $description): void 70 | { 71 | $this->description = $description; 72 | } 73 | 74 | /** 75 | * Sets the subgroups. Keep in mind that the property is called "subgroup" 76 | * although it can hold several subgroups. 77 | * 78 | * @param ObjectStorage $subgroup 79 | */ 80 | public function setSubgroup(ObjectStorage $subgroup): void 81 | { 82 | $this->subgroup = $subgroup; 83 | } 84 | 85 | /** 86 | * Adds a subgroup to the frontend user 87 | */ 88 | public function addSubgroup(FrontendUserGroup $subgroup): void 89 | { 90 | $this->subgroup?->attach($subgroup); 91 | } 92 | 93 | /** 94 | * Removes a subgroup from the frontend user group 95 | */ 96 | public function removeSubgroup(FrontendUserGroup $subgroup): void 97 | { 98 | $this->subgroup?->detach($subgroup); 99 | } 100 | 101 | /** 102 | * Returns the subgroups. Keep in mind that the property is called "subgroup" 103 | * although it can hold several subgroups. 104 | * 105 | * @return ?ObjectStorage 106 | */ 107 | public function getSubgroup(): ?ObjectStorage 108 | { 109 | return $this->subgroup; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Info.php: -------------------------------------------------------------------------------- 1 | name; 34 | } 35 | 36 | public function setName(string $name): void 37 | { 38 | $this->name = $name; 39 | } 40 | 41 | public function getBodytext(): string 42 | { 43 | return $this->bodytext; 44 | } 45 | 46 | public function setBodytext(string $bodytext): void 47 | { 48 | $this->bodytext = $bodytext; 49 | } 50 | 51 | public function getCombinedString(): string 52 | { 53 | return $this->name . ': ' . $this->bodytext; 54 | } 55 | 56 | public function __toString(): string 57 | { 58 | return $this->name; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Person.php: -------------------------------------------------------------------------------- 1 | 'StringLength', 'options' => ['maximum' => 80]])] 31 | protected string $firstname = ''; 32 | 33 | #[Validate(['validator' => 'StringLength', 'options' => ['minimum' => 2, 'maximum' => 80]])] 34 | protected string $lastname = ''; 35 | 36 | #[Transient()] 37 | protected string $fullname = ''; 38 | 39 | #[Validate(['validator' => 'EmailAddress'])] 40 | protected string $email = ''; 41 | 42 | /** 43 | * @var ?ObjectStorage 44 | */ 45 | protected ?ObjectStorage $tags = null; 46 | 47 | /** 48 | * @var ?ObjectStorage 49 | */ 50 | protected ?ObjectStorage $tagsSpecial = null; 51 | 52 | /** 53 | * Constructs a new Person 54 | */ 55 | public function __construct(string $firstname, string $lastname, string $email) 56 | { 57 | $this->setFirstname($firstname); 58 | $this->setLastname($lastname); 59 | $this->setEmail($email); 60 | 61 | $this->initializeObject(); 62 | } 63 | 64 | /** 65 | * Initializes all ObjectStorage properties when model is reconstructed from DB (where __construct is not called) 66 | */ 67 | public function initializeObject(): void 68 | { 69 | $this->tags ??= new ObjectStorage(); 70 | $this->tagsSpecial ??= new ObjectStorage(); 71 | } 72 | 73 | public function getFirstname(): string 74 | { 75 | return $this->firstname; 76 | } 77 | 78 | public function setFirstname(string $firstname): void 79 | { 80 | $this->firstname = $firstname; 81 | } 82 | 83 | public function getLastname(): string 84 | { 85 | return $this->lastname; 86 | } 87 | 88 | public function setLastname(string $lastname): void 89 | { 90 | $this->lastname = $lastname; 91 | } 92 | 93 | public function getEmail(): string 94 | { 95 | return $this->email; 96 | } 97 | 98 | public function setEmail(string $email): void 99 | { 100 | $this->email = $email; 101 | } 102 | 103 | public function getFullName(): string 104 | { 105 | $this->fullname = $this->firstname . ' ' . $this->lastname; 106 | return $this->fullname; 107 | } 108 | 109 | /** 110 | * @return ObjectStorage 111 | */ 112 | public function getTags(): ObjectStorage 113 | { 114 | return $this->tags; 115 | } 116 | 117 | /** 118 | * @param ObjectStorage $tags 119 | */ 120 | public function setTags(ObjectStorage $tags): void 121 | { 122 | $this->tags = $tags; 123 | } 124 | 125 | public function addTag(Tag $tag): void 126 | { 127 | $this->tags->attach($tag); 128 | } 129 | 130 | public function removeTag(Tag $tag): void 131 | { 132 | $this->tags->detach($tag); 133 | } 134 | 135 | /** 136 | * @return ObjectStorage 137 | */ 138 | public function getTagsSpecial(): ObjectStorage 139 | { 140 | return $this->tagsSpecial; 141 | } 142 | 143 | /** 144 | * @param ObjectStorage $tagsSpecial 145 | */ 146 | public function setTagsSpecial(ObjectStorage $tagsSpecial): void 147 | { 148 | $this->tagsSpecial = $tagsSpecial; 149 | } 150 | 151 | public function addTagSpecial(Tag $tag): void 152 | { 153 | $this->tagsSpecial->attach($tag); 154 | } 155 | 156 | public function removeTagSpecial(Tag $tag): void 157 | { 158 | $this->tagsSpecial->detach($tag); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Post.php: -------------------------------------------------------------------------------- 1 | 'StringLength', 'options' => ['minimum' => 3, 'maximum' => 50]])] 34 | protected string $title = ''; 35 | 36 | /** 37 | * @var \DateTime 38 | */ 39 | protected ?\DateTime $date = null; 40 | 41 | /** 42 | * @var Person 43 | */ 44 | protected ?Person $author = null; 45 | protected ?Person $secondAuthor = null; 46 | protected ?Person $reviewer = null; 47 | 48 | #[Validate(['validator' => 'StringLength', 'options' => ['minimum' => 3]])] 49 | protected string $content = ''; 50 | 51 | /** 52 | * @var ?ObjectStorage 53 | */ 54 | public ?ObjectStorage $tags = null; 55 | 56 | /** 57 | * @var ?ObjectStorage 58 | */ 59 | public ?ObjectStorage $categories = null; 60 | 61 | /** 62 | * @var ?ObjectStorage 63 | */ 64 | #[Lazy()] 65 | #[Cascade(['value' => 'remove'])] 66 | public ?ObjectStorage $comments = null; 67 | 68 | /** 69 | * @var ?ObjectStorage 70 | */ 71 | #[Lazy()] 72 | public ?ObjectStorage $relatedPosts = null; 73 | 74 | /** 75 | * 1:1 optional relation 76 | */ 77 | #[Cascade(['value' => 'remove'])] 78 | public ?Info $additionalName = null; 79 | 80 | /** 81 | * 1:1 optional relation 82 | */ 83 | #[Cascade(['value' => 'remove'])] 84 | protected ?Info $additionalInfo = null; 85 | 86 | /** 87 | * 1:n relation stored as CSV value 88 | * @var ?ObjectStorage 89 | */ 90 | #[Lazy()] 91 | public ?ObjectStorage $additionalComments = null; 92 | 93 | public function __construct() 94 | { 95 | $this->initializeObject(); 96 | } 97 | 98 | /** 99 | * Initializes all ObjectStorage properties when model is reconstructed from DB (where __construct is not called) 100 | */ 101 | public function initializeObject(): void 102 | { 103 | $this->tags ??= new ObjectStorage(); 104 | $this->categories ??= new ObjectStorage(); 105 | $this->comments ??= new ObjectStorage(); 106 | $this->relatedPosts ??= new ObjectStorage(); 107 | $this->additionalComments ??= new ObjectStorage(); 108 | } 109 | 110 | /** 111 | * Set one or more Tag objects 112 | * 113 | * @param ObjectStorage $tags 114 | */ 115 | public function setTags(ObjectStorage $tags): void 116 | { 117 | $this->tags = $tags; 118 | } 119 | 120 | public function addTag(Tag $tag): void 121 | { 122 | $this->tags?->attach($tag); 123 | } 124 | 125 | public function removeTag(Tag $tag): void 126 | { 127 | $this->tags?->detach($tag); 128 | } 129 | 130 | public function removeAllTags(): void 131 | { 132 | $this->tags = new ObjectStorage(); 133 | } 134 | 135 | /** 136 | * Getter for tags, A storage holding objects 137 | * Note: We return a clone of the tags because they must not be modified as they are Value Objects 138 | * 139 | * @return ObjectStorage 140 | */ 141 | public function getTags(): ?ObjectStorage 142 | { 143 | return clone $this->tags; 144 | } 145 | 146 | /** 147 | * Add category to a post 148 | */ 149 | public function addCategory(Category $category): void 150 | { 151 | $this->categories?->attach($category); 152 | } 153 | 154 | /** 155 | * Set categories 156 | * 157 | * @param ObjectStorage $categories 158 | */ 159 | public function setCategories(ObjectStorage $categories): void 160 | { 161 | $this->categories = $categories; 162 | } 163 | 164 | /** 165 | * Get categories 166 | * 167 | * @return ?ObjectStorage 168 | */ 169 | public function getCategories(): ?ObjectStorage 170 | { 171 | return $this->categories; 172 | } 173 | 174 | /** 175 | * Remove category from post 176 | */ 177 | public function removeCategory(Category $category): void 178 | { 179 | $this->categories?->detach($category); 180 | } 181 | 182 | /** 183 | * Sets the author for this post 184 | */ 185 | public function setAuthor(Person $author): void 186 | { 187 | $this->author = $author; 188 | } 189 | 190 | /** 191 | * Getter for author 192 | */ 193 | public function getAuthor(): ?Person 194 | { 195 | return $this->author; 196 | } 197 | 198 | public function getSecondAuthor(): ?Person 199 | { 200 | return $this->secondAuthor; 201 | } 202 | 203 | public function setSecondAuthor(Person $secondAuthor): void 204 | { 205 | $this->secondAuthor = $secondAuthor; 206 | } 207 | 208 | public function getReviewer(): ?Person 209 | { 210 | return $this->reviewer; 211 | } 212 | 213 | public function setReviewer(Person $reviewer): void 214 | { 215 | $this->reviewer = $reviewer; 216 | } 217 | 218 | /** 219 | * Set the comments to this post, an Object Storage of related Comment instances 220 | * 221 | * @param ObjectStorage $comments 222 | */ 223 | public function setComments(ObjectStorage $comments): void 224 | { 225 | $this->comments = $comments; 226 | } 227 | 228 | /** 229 | * Adds a comment to this post 230 | */ 231 | public function addComment(Comment $commentToAdd): void 232 | { 233 | $this->comments?->attach($commentToAdd); 234 | } 235 | 236 | /** 237 | * Removes Comment from this post and deletes it due to annotation `@Cascade("remove")` 238 | */ 239 | public function removeComment(Comment $commentToDelete): void 240 | { 241 | $this->comments?->detach($commentToDelete); 242 | } 243 | 244 | /** 245 | * @TODO: explain what's done in the method an why 246 | */ 247 | public function removeAllComments(): void 248 | { 249 | $comments = clone $this->comments; 250 | $this->comments->removeAll($comments); 251 | } 252 | 253 | /** 254 | * Returns the comments to this post 255 | * 256 | * @return ObjectStorage 257 | */ 258 | public function getComments(): ?ObjectStorage 259 | { 260 | return $this->comments; 261 | } 262 | 263 | /** 264 | * Set the related posts with ObjectStorage containing the instances of relatedPosts 265 | * 266 | * @param ObjectStorage $relatedPosts 267 | */ 268 | public function setRelatedPosts(ObjectStorage $relatedPosts): void 269 | { 270 | $this->relatedPosts = $relatedPosts; 271 | } 272 | 273 | public function addRelatedPost(Post $post): void 274 | { 275 | $this->relatedPosts?->attach($post); 276 | } 277 | 278 | public function removeAllRelatedPosts(): void 279 | { 280 | $relatedPosts = clone $this->relatedPosts; 281 | $this->relatedPosts->removeAll($relatedPosts); 282 | } 283 | 284 | /** 285 | * Returns the related posts 286 | * 287 | * @return ObjectStorage 288 | */ 289 | public function getRelatedPosts(): ?ObjectStorage 290 | { 291 | return $this->relatedPosts; 292 | } 293 | 294 | /** 295 | * @return ObjectStorage 296 | */ 297 | public function getAdditionalComments(): ?ObjectStorage 298 | { 299 | return $this->additionalComments; 300 | } 301 | 302 | /** 303 | * @param ObjectStorage $additionalComments 304 | */ 305 | public function setAdditionalComments(ObjectStorage $additionalComments): void 306 | { 307 | $this->additionalComments = $additionalComments; 308 | } 309 | 310 | public function addAdditionalComment(Comment $comment): void 311 | { 312 | $this->additionalComments?->attach($comment); 313 | } 314 | 315 | /** 316 | * Remove all additional Comments 317 | */ 318 | public function removeAllAdditionalComments(): void 319 | { 320 | $comments = clone $this->additionalComments; 321 | $this->additionalComments->removeAll($comments); 322 | } 323 | 324 | public function removeAdditionalComment(Comment $comment): void 325 | { 326 | $this->additionalComments?->detach($comment); 327 | } 328 | 329 | /** 330 | * @return Blog 331 | */ 332 | public function getBlog(): Blog 333 | { 334 | return $this->blog; 335 | } 336 | 337 | public function setBlog(Blog $blog): void 338 | { 339 | $this->blog = $blog; 340 | } 341 | 342 | /** 343 | * @return string 344 | */ 345 | public function getTitle(): string 346 | { 347 | return $this->title; 348 | } 349 | 350 | public function setTitle(string $title): void 351 | { 352 | $this->title = $title; 353 | } 354 | 355 | public function getDate(): ?\DateTime 356 | { 357 | return $this->date; 358 | } 359 | 360 | public function setDate(\DateTime $date): void 361 | { 362 | $this->date = $date; 363 | } 364 | 365 | /** 366 | * @return string 367 | */ 368 | public function getContent(): string 369 | { 370 | return $this->content; 371 | } 372 | 373 | public function setContent(string $content): void 374 | { 375 | $this->content = $content; 376 | } 377 | 378 | /** 379 | * @return Info|null 380 | */ 381 | public function getAdditionalName(): ?Info 382 | { 383 | return $this->additionalName; 384 | } 385 | 386 | public function setAdditionalName(?Info $additionalName): void 387 | { 388 | $this->additionalName = $additionalName; 389 | } 390 | 391 | public function getAdditionalInfo(): ?Info 392 | { 393 | return $this->additionalInfo; 394 | } 395 | 396 | public function setAdditionalInfo(?Info $additionalInfo): void 397 | { 398 | $this->additionalInfo = $additionalInfo; 399 | } 400 | 401 | /** 402 | * Returns this post as a formatted string 403 | */ 404 | public function __toString(): string 405 | { 406 | return $this->title . chr(10) . 407 | ' written on ' . $this->date->format('Y-m-d') . chr(10) . 408 | ' by ' . $this->author->getFullName() . chr(10) . 409 | wordwrap($this->content, 70, chr(10)) . chr(10) . 410 | implode(', ', $this->tags->toArray()); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Tag.php: -------------------------------------------------------------------------------- 1 | name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/AdministratorRepository.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class AdministratorRepository extends Repository {} 28 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/BlogRepository.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class BlogRepository extends Repository 29 | { 30 | protected $defaultOrderings = [ 31 | 'crdate' => QueryInterface::ORDER_DESCENDING, 32 | 'uid' => QueryInterface::ORDER_DESCENDING, 33 | ]; 34 | } 35 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/CommentRepository.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class CommentRepository extends Repository 29 | { 30 | protected $defaultOrderings = ['date' => QueryInterface::ORDER_DESCENDING]; 31 | 32 | public function initializeObject(): void 33 | { 34 | $querySettings = $this->createQuery()->getQuerySettings(); 35 | // Show comments from all pages 36 | $querySettings->setRespectStoragePage(false); 37 | $this->setDefaultQuerySettings($querySettings); 38 | } 39 | 40 | /** 41 | * @return QueryResultInterface 42 | */ 43 | public function findAllIgnoreEnableFields(): QueryResultInterface 44 | { 45 | $query = $this->createQuery(); 46 | $query->getQuerySettings()->setIgnoreEnableFields(true); 47 | return $query->execute(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/PersonRepository.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class PersonRepository extends Repository 28 | { 29 | protected $defaultOrderings = ['lastname' => QueryInterface::ORDER_ASCENDING]; 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/PostRepository.php: -------------------------------------------------------------------------------- 1 | 30 | * 31 | * @method Post findByUid($uid) 32 | */ 33 | class PostRepository extends Repository 34 | { 35 | protected $defaultOrderings = ['date' => QueryInterface::ORDER_DESCENDING]; 36 | 37 | /** 38 | * Finds all posts by the specified blog 39 | * 40 | * @return QueryResultInterface 41 | */ 42 | public function findAllByBlog(Blog $blog): QueryResultInterface 43 | { 44 | $query = $this->createQuery(); 45 | return $query 46 | ->matching( 47 | $query->equals('blog', $blog), 48 | ) 49 | ->execute(); 50 | } 51 | 52 | /** 53 | * Finds posts by the specified tag and blog 54 | * 55 | * @param Blog $blog The blog the post must refer to 56 | * @return QueryResultInterface 57 | */ 58 | public function findByTagAndBlog( 59 | string $tag, 60 | Blog $blog, 61 | ): QueryResultInterface { 62 | $query = $this->createQuery(); 63 | return $query 64 | ->matching( 65 | $query->logicalAnd( 66 | $query->equals('blog', $blog), 67 | $query->equals('tags.name', $tag), 68 | ), 69 | ) 70 | ->execute(); 71 | } 72 | 73 | /** 74 | * Finds all remaining posts of the blog 75 | * 76 | * @param Post $post The reference post 77 | * @return QueryResultInterface 78 | */ 79 | public function findRemaining(Post $post): QueryResultInterface 80 | { 81 | $blog = $post->getBlog(); 82 | $query = $this->createQuery(); 83 | return $query 84 | ->matching( 85 | $query->logicalAnd( 86 | $query->equals('blog', $blog), 87 | $query->logicalNot( 88 | $query->equals('uid', $post->getUid()), 89 | ), 90 | ), 91 | ) 92 | ->execute(); 93 | } 94 | 95 | /** 96 | * Finds the previous of the given post 97 | * 98 | * @param Post $post The reference post 99 | */ 100 | public function findPrevious(Post $post): ?Post 101 | { 102 | $query = $this->createQuery(); 103 | return $query 104 | ->matching( 105 | $query->lessThan('date', $post->getDate()), 106 | ) 107 | ->execute() 108 | ->getFirst(); 109 | } 110 | 111 | /** 112 | * Finds the post next to the given post 113 | * 114 | * @param Post $post The reference post 115 | */ 116 | public function findNext(Post $post): ?Post 117 | { 118 | $query = $this->createQuery(); 119 | return $query 120 | ->matching( 121 | $query->greaterThan('date', $post->getDate()), 122 | ) 123 | ->execute() 124 | ->getFirst(); 125 | } 126 | 127 | /** 128 | * Finds most recent posts by the specified blog 129 | * 130 | * @param Blog $blog The blog the post must refer to 131 | * @param int $limit The number of posts to return at max 132 | * @return QueryResultInterface 133 | */ 134 | public function findRecentByBlog( 135 | Blog $blog, 136 | int $limit = 5, 137 | ): QueryResultInterface { 138 | $query = $this->createQuery(); 139 | return $query 140 | ->matching( 141 | $query->equals('blog', $blog), 142 | ) 143 | ->setLimit((int)$limit) 144 | ->execute(); 145 | } 146 | 147 | /** 148 | * @return QueryResultInterface 149 | * @throws InvalidQueryException 150 | */ 151 | public function findByCategory(int $categoryUid): QueryResultInterface 152 | { 153 | $query = $this->createQuery(); 154 | return $query 155 | ->matching( 156 | $query->contains('categories', $categoryUid), 157 | ) 158 | ->execute(); 159 | } 160 | 161 | /** 162 | * @param int[] $uids 163 | * @return QueryResultInterface 164 | * @throws InvalidQueryException 165 | */ 166 | public function findAllSortedByCategory(array $uids): QueryResultInterface 167 | { 168 | $q = $this->createQuery(); 169 | $q->matching($q->in('uid', $uids)); 170 | $q->setOrderings([ 171 | 'categories.title' => QueryInterface::ORDER_ASCENDING, 172 | 'uid' => QueryInterface::ORDER_ASCENDING, 173 | ]); 174 | return $q->execute(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Classes/Domain/Validator/BlogValidator.php: -------------------------------------------------------------------------------- 1 | addError($errorString, 1297418975); 39 | } 40 | if (!$this->blogValidationService->isBlogCategoryCountValid($value)) { 41 | $errorString = LocalizationUtility::translate( 42 | 'error.Blog.tooManyCategories', 43 | 'BlogExample', 44 | ); 45 | // Add the error to the property if it is specific to one property 46 | $this->addErrorForProperty('categories', $errorString, 1297418976); 47 | } 48 | if (!$this->blogValidationService->isBlogSubtitleValid($value)) { 49 | $errorString = LocalizationUtility::translate( 50 | 'error.Blog.invalidSubTitle', 51 | 'BlogExample', 52 | ); 53 | // Add the error directly if it takes several properties into account 54 | $this->addError($errorString, 1297418974); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Classes/Domain/Validator/PostValidator.php: -------------------------------------------------------------------------------- 1 | postValidationService->isTitleValid($value)) { 41 | $error = new Error('Title custom validation failed', 1480872650); 42 | $this->result->forProperty('title')->addError($error); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Classes/Domain/Validator/TitleValidator.php: -------------------------------------------------------------------------------- 1 | addError($errorString, 1297418976); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Exception/NoBlogAdminAccessException.php: -------------------------------------------------------------------------------- 1 | expressionLanguageVariables = [ 20 | 'blogConfiguration' => $configuration->get('blog_example'), 21 | ]; 22 | } catch (ExtensionConfigurationExtensionNotConfiguredException | ExtensionConfigurationPathDoesNotExistException) { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Classes/PageTitle/BlogPageTitleProvider.php: -------------------------------------------------------------------------------- 1 | title = $title; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Classes/Property/TypeConverters/HiddenCommentConverter.php: -------------------------------------------------------------------------------- 1 | persistenceManager->createQueryForType($targetType); 35 | $query 36 | ->getQuerySettings() 37 | ->setRespectStoragePage(false) 38 | ->setIgnoreEnableFields(true) 39 | ->setEnableFieldsToBeIgnored(['disabled']); 40 | $object = $query->matching($query->equals('uid', $identity))->execute()->getFirst(); 41 | } else { 42 | throw new InvalidSourceException( 43 | 'The identity property "' . $identity . '" is no UID.', 44 | 1641904861, 45 | ); 46 | } 47 | 48 | if ($object === null) { 49 | throw new TargetNotFoundException( 50 | sprintf('Object of type %s with identity "%s" not found.', $targetType, print_r($identity, true)), 51 | 1641904896, 52 | ); 53 | } 54 | 55 | return $object; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Classes/Service/BlogFactory.php: -------------------------------------------------------------------------------- 1 | setTitle('Blog #' . $blogNumber); 45 | $blog->description = 'A blog about TYPO3 extension development.'; 46 | 47 | // create author 48 | $author = new Person('Stephen', 'Smith', 'foo.bar@example.com'); 49 | 50 | // create administrator 51 | $administrator = new Administrator(); 52 | $administrator->name = 'John Doe'; 53 | $administrator->email = 'john.doe@example.com'; 54 | $blog->administrator = $administrator; 55 | 56 | // create sample posts 57 | for ($postNumber = 1; $postNumber < 6; $postNumber++) { 58 | // create post 59 | $post = new Post(); 60 | $post->setTitle('The ' . $postNumber . '. post of blog #' . $blogNumber); 61 | $post->setAuthor($author); 62 | $post->setContent('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'); 63 | 64 | // create comments 65 | $comment = new Comment(); 66 | $comment->setDate(new \DateTime()); 67 | $comment->setAuthor('Peter Pan'); 68 | $comment->setEmail('peter.pan@example.com'); 69 | $comment->setContent('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.'); 70 | $post->addComment($comment); 71 | 72 | $comment = new Comment(); 73 | $comment->setDate(new \DateTime('2009-03-19 23:44')); 74 | $comment->setAuthor('John Smith'); 75 | $comment->setEmail('john@matrix.org'); 76 | $comment->setContent('Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.'); 77 | $post->addComment($comment); 78 | 79 | // create some random tags 80 | if (random_int(0, 1) > 0) { 81 | $tag = new Tag('MVC'); 82 | $post->addTag($tag); 83 | } 84 | if (random_int(0, 1) > 0) { 85 | $tag = new Tag('Domain Driven Design'); 86 | $post->addTag($tag); 87 | } 88 | if (random_int(0, 1) > 0) { 89 | $tag = new Tag('TYPO3'); 90 | $post->addTag($tag); 91 | } 92 | // add the post to the current blog 93 | $blog->addPost($post); 94 | $post->setBlog($blog); 95 | } 96 | return $blog; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Classes/Service/BlogValidationService.php: -------------------------------------------------------------------------------- 1 | maxCategoryCount = $maxCategoryCount; 29 | } 30 | 31 | public function isBlogCategoryCountValid(Blog $blog): bool 32 | { 33 | return $blog->getCategories()->count() <= $this->maxCategoryCount; 34 | } 35 | 36 | public function isBlogSubtitleValid(Blog $blog): bool 37 | { 38 | return strtolower($blog->getTitle()) !== strtolower($blog->subtitle ?? ''); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/Service/PostValidationService.php: -------------------------------------------------------------------------------- 1 | getTitle(), $this->forbiddenTitles); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/Upgrades/MigratePluginsToContentElementsUpgradeWizard.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 36 | 37 | $queryBuilder 38 | ->update('tt_content') 39 | ->set('CType', $pluginName) 40 | ->set('list_type', '') 41 | ->where( 42 | $queryBuilder->expr()->eq( 43 | 'CType', 44 | $queryBuilder->createNamedParameter('list'), 45 | ), 46 | $queryBuilder->expr()->eq( 47 | 'list_type', 48 | $queryBuilder->createNamedParameter($pluginName), 49 | ), 50 | ) 51 | ->executeStatement(); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | public function updateNecessary(): bool 58 | { 59 | $queryBuilder = $this->getQueryBuilder(); 60 | 61 | $orConstraints = []; 62 | foreach (self::PLUGINS as $pluginName) { 63 | $orConstraints[] = $queryBuilder->expr()->eq( 64 | 'list_type', 65 | $queryBuilder->createNamedParameter($pluginName), 66 | ); 67 | } 68 | 69 | return (bool)$queryBuilder 70 | ->count('*') 71 | ->from('tt_content') 72 | ->where( 73 | $queryBuilder->expr()->eq( 74 | 'CType', 75 | $queryBuilder->createNamedParameter('list'), 76 | ), 77 | $queryBuilder->expr()->or(...$orConstraints), 78 | ) 79 | ->executeQuery() 80 | ->fetchOne(); 81 | } 82 | 83 | private function getQueryBuilder(): QueryBuilder 84 | { 85 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 86 | ->getQueryBuilderForTable('tt_content'); 87 | 88 | $queryBuilder 89 | ->getRestrictions() 90 | ->removeAll() 91 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 92 | 93 | return $queryBuilder; 94 | } 95 | 96 | public function getPrerequisites(): array 97 | { 98 | return []; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Classes/Upgrades/PluginUpgradeWizard.php: -------------------------------------------------------------------------------- 1 | connectionPool->getQueryBuilderForTable('tt_content'); 31 | $result = $queryBuilder 32 | ->update('tt_content') 33 | ->where( 34 | $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter(self::OLD_LIST_TYPE)), 35 | ) 36 | ->set('list_type', self::NEW_LIST_TYPE) 37 | ->executeStatement(); 38 | return $result > 0; 39 | } 40 | 41 | public function updateNecessary(): bool 42 | { 43 | $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); 44 | $count = $queryBuilder 45 | ->count('uid') 46 | ->from('tt_content') 47 | ->where( 48 | $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter(self::OLD_LIST_TYPE)), 49 | ) 50 | ->executeQuery() 51 | ->fetchOne(); 52 | return is_int($count) && $count > 0; 53 | } 54 | 55 | public function getPrerequisites(): array 56 | { 57 | return []; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/GravatarViewHelper.php: -------------------------------------------------------------------------------- 1 | 30 | * 35 | * 36 | * 37 | * 38 | * 39 | */ 40 | class GravatarViewHelper extends AbstractTagBasedViewHelper 41 | { 42 | /** 43 | * @var string 44 | */ 45 | protected $tagName = 'img'; 46 | public function __construct(private readonly UriFactory $uriFactory) 47 | { 48 | parent::__construct(); 49 | } 50 | 51 | public function initializeArguments(): void 52 | { 53 | $this->registerArgument('emailAddress', 'string', '', true); 54 | $this->registerArgument('defaultImageUri', 'string', ''); 55 | $this->registerArgument('size', 'int', ''); 56 | } 57 | 58 | public function render(): string 59 | { 60 | $gravatarUri = $this->uriFactory->createUri( 61 | 'https://gravatar.com/avatar/' . md5($this->arguments['emailAddress']), 62 | ); 63 | 64 | $queryArguments = []; 65 | if ($this->arguments['defaultImageUri'] !== null) { 66 | $queryArguments['d'] = urlencode($this->arguments['defaultImageUri']); 67 | } 68 | 69 | if ($this->arguments['size'] !== null) { 70 | $queryArguments['s'] = MathUtility::forceIntegerInRange((int)$this->arguments['size'], 1, 2048); 71 | } 72 | 73 | $this->tag->addAttribute( 74 | 'src', 75 | (string)$gravatarUri->withQuery(HttpUtility::buildQueryString($queryArguments, '', true)), 76 | ); 77 | 78 | return $this->tag->render(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Configuration/Backend/Modules.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'parent' => 'web', 11 | 'position' => ['after' => 'web_info'], 12 | 'access' => 'user', 13 | 'workspaces' => 'live', 14 | 'path' => '/module/page/blog_example', 15 | 'labels' => 'LLL:EXT:blog_example/Resources/Private/Language/Module/locallang_mod.xlf', 16 | 'extensionName' => 'BlogExample', 17 | 'controllerActions' => [ 18 | BackendController::class => [ 19 | 'index', 20 | 'deleteAll', 21 | 'deleteBlog', 22 | 'populate', 23 | 'showBlog', 24 | 'showPost', 25 | 'showAllComments', 26 | ], 27 | ], 28 | ], 29 | ]; 30 | -------------------------------------------------------------------------------- /Configuration/ExpressionLanguage.php: -------------------------------------------------------------------------------- 1 | [ 11 | ExtensionConfigurationProvider::class, 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /Configuration/Extbase/Persistence/Classes.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'tableName' => 'fe_users', 13 | 'recordType' => Administrator::class, 14 | 'properties' => [ 15 | 'administratorName' => [ 16 | 'fieldName' => 'username', 17 | ], 18 | ], 19 | ], 20 | FrontendUserGroup::class => [ 21 | 'tableName' => 'fe_groups', 22 | ], 23 | Blog::class => [ 24 | 'tableName' => 'tx_blogexample_domain_model_blog', 25 | 'properties' => [ 26 | 'categories' => [ 27 | 'fieldName' => 'category', 28 | ], 29 | ], 30 | ], 31 | Post::class => [ 32 | 'tableName' => 'tx_blogexample_domain_model_post', 33 | 'properties' => [ 34 | 'categories' => [ 35 | 'fieldName' => 'category', 36 | ], 37 | ], 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /Configuration/FlexForms/PluginSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Options 6 | array 7 | 8 | 9 | 10 | 11 | number 12 | 2 13 | 3 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Configuration/Icons.php: -------------------------------------------------------------------------------- 1 | [ 9 | // icon provider class 10 | 'provider' => SvgIconProvider::class, 11 | // the source SVG for the SvgIconProvider 12 | 'source' => 'EXT:blog_example/Resources/Public/Icons/Extension.svg', 13 | ], 14 | 'blogexample_blog' => [ 15 | 'provider' => BitmapIconProvider::class, 16 | 'source' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_blog.gif', 17 | ], 18 | 'blogexample_comment' => [ 19 | 'provider' => BitmapIconProvider::class, 20 | 'source' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_comment.gif', 21 | ], 22 | 'blogexample_person' => [ 23 | 'provider' => BitmapIconProvider::class, 24 | 'source' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_person.gif', 25 | ], 26 | 'blogexample_post' => [ 27 | 'provider' => BitmapIconProvider::class, 28 | 'source' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_post.gif', 29 | ], 30 | 'blogexample_tag' => [ 31 | 'provider' => BitmapIconProvider::class, 32 | 'source' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif', 33 | ], 34 | ]; 35 | -------------------------------------------------------------------------------- /Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | T3docs\BlogExample\: 8 | resource: '../Classes/*' 9 | exclude: '../Classes/Domain/Model/*' 10 | T3docs\BlogExample\Property\TypeConverters\HiddenCommentConverter: 11 | tags: 12 | - name: extbase.type_converter 13 | priority: 2 14 | target: T3docs\BlogExample\Domain\Model\Comment 15 | sources: int,string 16 | -------------------------------------------------------------------------------- /Configuration/Sets/BlogExample/config.yaml: -------------------------------------------------------------------------------- 1 | name: t3docs/blog-example 2 | label: Blog example set 3 | -------------------------------------------------------------------------------- /Configuration/Sets/BlogExample/settings.definitions.yaml: -------------------------------------------------------------------------------- 1 | categories: 2 | BlogExample: 3 | label: 'Blog Example' 4 | BlogExample.templates: 5 | label: 'Templates' 6 | parent: BlogExample 7 | BlogExample.pages: 8 | label: 'Pages' 9 | parent: BlogExample 10 | BlogExample.layout: 11 | label: 'Layout' 12 | parent: BlogExample 13 | BlogExample.settings: 14 | label: 'Settings' 15 | parent: BlogExample 16 | 17 | settings: 18 | blogExample.templateRootPath: 19 | label: 'Templates' 20 | category: BlogExample.templates 21 | description: 'Path to template root' 22 | type: string 23 | default: 'EXT:blog_example/Resources/Private/Templates/' 24 | blogExample.partialRootPath: 25 | label: 'Partials' 26 | category: BlogExample.templates 27 | description: 'Path to partial root' 28 | type: string 29 | default: 'EXT:blog_example/Resources/Private/Partials/' 30 | blogExample.layoutRootPath: 31 | label: 'Layouts' 32 | category: BlogExample.templates 33 | description: 'Path to layout root' 34 | type: string 35 | default: 'EXT:blog_example/Resources/Private/Layouts/' 36 | blogExample.storagePid: 37 | label: 'Default storage PID' 38 | category: BlogExample.pages 39 | description: 'Folder that stores all blogs, blog-posts and comments' 40 | type: int 41 | default: 0 42 | blogExample.editorUsergroupUid: 43 | label: 'Frontend admin user group UID' 44 | category: BlogExample.settings 45 | description: 'Enter the uid of the front end user group that should be allowed to edit blogs and post in the frontend in the admin plugin. ' 46 | type: int 47 | default: 0 48 | blogExample.postsPerPage: 49 | label: 'Posts per page' 50 | category: BlogExample.settings 51 | description: 'How many posts should be displayed per page of pagination?' 52 | type: int 53 | default: 3 54 | blogExample.defaultGravator: 55 | label: 'Default gravator icon' 56 | category: BlogExample.layout 57 | description: 'This icon is displayed for a comment if the email address of the commentary is not registered. ' 58 | type: string 59 | default: 'EXT:blog_example/Resources/Public/Icons/default_gravatar.gif' 60 | -------------------------------------------------------------------------------- /Configuration/Sets/BlogExample/setup.typoscript: -------------------------------------------------------------------------------- 1 | # Plugin configuration 2 | plugin.tx_blogexample { 3 | settings { 4 | postsPerPage = {$blogExample.postsPerPage} 5 | editorUsergroupUid = {$blogExample.editorUsergroupUid} 6 | defaultGravator = {$blogExample.defaultGravator} 7 | } 8 | 9 | persistence { 10 | storagePid = {$blogExample.storagePid} 11 | } 12 | 13 | view { 14 | templateRootPaths.10 = {$blogExample.templateRootPath} 15 | partialRootPaths.10 = {$blogExample.partialRootPath} 16 | layoutRootPaths.10 = {$blogExample.layoutRootPath} 17 | defaultPid = auto 18 | } 19 | 20 | # This is an example how to modify the translation 21 | _LOCAL_LANG { 22 | default { 23 | someUnusedKey = foo 24 | } 25 | } 26 | } 27 | 28 | # Make postlist available as lib.blog_example_post_list 29 | 30 | lib.blog_example_post_list < tt_content.list.20.blogexample_postlist 31 | 32 | # Configure page title provider for blog post indexAction and showAction 33 | config { 34 | pageTitleProviders { 35 | blogExample { 36 | provider = T3docs\BlogExample\PageTitle\BlogPageTitleProvider 37 | before = record 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Configuration/Sets/DefaultStyles/config.yaml: -------------------------------------------------------------------------------- 1 | name: t3docs/blog-example-styles 2 | label: Blog example default styles 3 | dependencies: 4 | - t3docs/blog-example 5 | -------------------------------------------------------------------------------- /Configuration/Sets/DefaultStyles/setup.typoscript: -------------------------------------------------------------------------------- 1 | # Include BlogExample default styles 2 | 3 | page.includeCSS.tx_blogexample = EXT:blog_example/Resources/Public/Css/BlogExample.css 4 | -------------------------------------------------------------------------------- /Configuration/Sets/RssFeed/config.yaml: -------------------------------------------------------------------------------- 1 | name: t3docs/blog-example-rss 2 | label: Blog example RSS feed 3 | dependencies: 4 | - t3docs/blog-example 5 | -------------------------------------------------------------------------------- /Configuration/Sets/RssFeed/constants.typoscript: -------------------------------------------------------------------------------- 1 | plugin.tx_blogexample { 2 | settings { 3 | # cat=plugin.tx_blogexample/a; type=int+; label=Rss page type:If the default RSS page typenum (778) conflicts with your setup, you can override this setting here 4 | rssPageType = 778 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Configuration/Sets/RssFeed/settings.definitions.yaml: -------------------------------------------------------------------------------- 1 | categories: 2 | BlogExample.rss: 3 | label: 'RSS feed' 4 | parent: BlogExample 5 | 6 | settings: 7 | blogExampleRss.page_type: 8 | label: 'RSS page type' 9 | category: BlogExample.rss 10 | description: 'If the default RSS page type number (778) conflicts with your setup, you can override this setting here. ' 11 | type: int 12 | default: 778 13 | blogExampleRss.title: 14 | label: 'Title of the RSS feed' 15 | category: BlogExample.rss 16 | description: 'Path to template root' 17 | type: string 18 | default: 'The RSS feed' 19 | -------------------------------------------------------------------------------- /Configuration/Sets/RssFeed/setup.typoscript: -------------------------------------------------------------------------------- 1 | plugin.tx_blogexample { 2 | settings { 3 | rss { 4 | channel { 5 | typeNum = {$blogExampleRss.page_type} 6 | title = {$blogExampleRss.title} 7 | } 8 | } 9 | } 10 | } 11 | # RSS rendering 12 | tx_blogexample_rss = PAGE 13 | tx_blogexample_rss { 14 | typeNum = {$blogExampleRss.page_type} 15 | 10 < tt_content.blogexample_postlistrss 16 | 17 | config { 18 | disableAllHeaderCode = 1 19 | xhtml_cleaning = none 20 | admPanel = 0 21 | debug = 0 22 | disablePrefixComment = 1 23 | metaCharset = utf-8 24 | additionalHeaders.10.header = Content-Type:application/rss+xml;charset=utf-8 25 | linkVars > 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Configuration/TCA/Overrides/fe_users.php: -------------------------------------------------------------------------------- 1 | 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:fe_users.tx_extbase_type.administrator', 13 | 'value' => Administrator::class, 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /Configuration/TCA/Overrides/tt_content.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog', 6 | 'label' => 'title', 7 | 'tstamp' => 'tstamp', 8 | 'crdate' => 'crdate', 9 | 'versioningWS' => true, 10 | 'origUid' => 't3_origuid', 11 | 'transOrigPointerField' => 'l10n_parent', 12 | 'transOrigDiffSourceField' => 'l10n_diffsource', 13 | 'languageField' => 'sys_language_uid', 14 | 'translationSource' => 'l10n_source', 15 | 'delete' => 'deleted', 16 | 'enablecolumns' => [ 17 | 'disabled' => 'hidden', 18 | 'fe_group' => 'fe_group', 19 | 'starttime' => 'starttime', 20 | 'endtime' => 'endtime', 21 | ], 22 | 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_blog.gif', 23 | ], 24 | 'columns' => [ 25 | 'title' => [ 26 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.title', 27 | 'config' => [ 28 | 'type' => 'input', 29 | 'size' => 20, 30 | 'eval' => 'trim', 31 | 'required' => true, 32 | 'max' => 256, 33 | ], 34 | ], 35 | 'subtitle' => [ 36 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.subtitle', 37 | 'config' => [ 38 | 'type' => 'input', 39 | 'size' => 20, 40 | 'eval' => 'trim', 41 | 'max' => 256, 42 | ], 43 | ], 44 | 'description' => [ 45 | 'exclude' => true, 46 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.description', 47 | 'config' => [ 48 | 'type' => 'text', 49 | 'enableRichtext' => true, 50 | 'required' => true, 51 | ], 52 | ], 53 | 'logo' => [ 54 | 'exclude' => true, 55 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.logo', 56 | 'config' => [ 57 | 'type' => 'file', 58 | 'allowed' => 'common-image-types', 59 | 'appearance' => [ 60 | 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference', 61 | ], 62 | ], 63 | ], 64 | 'posts' => [ 65 | 'exclude' => true, 66 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.posts', 67 | 'config' => [ 68 | 'type' => 'inline', 69 | 'foreign_table' => 'tx_blogexample_domain_model_post', 70 | 'foreign_field' => 'blog', 71 | 'foreign_sortby' => 'sorting', 72 | 'appearance' => [ 73 | 'collapseAll' => 1, 74 | 'expandSingle' => 1, 75 | ], 76 | ], 77 | ], 78 | 'administrator' => [ 79 | 'exclude' => true, 80 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.administrator', 81 | 'description' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.administrator.description', 82 | 'config' => [ 83 | 'type' => 'select', 84 | 'renderType' => 'selectSingle', 85 | 'foreign_table' => 'fe_users', 86 | 'foreign_table_where' => "AND fe_users.tx_extbase_type='T3docs\\\\BlogExample\\\\Domain\\\\Model\\\\Administrator'", 87 | 'items' => [ 88 | ['label' => '--none--', 'value' => 0], 89 | ], 90 | 'default' => 0, 91 | 'fieldControl' => [ 92 | 'editPopup' => [ 93 | 'disabled' => false, 94 | ], 95 | 'addRecord' => [ 96 | 'disabled' => false, 97 | 'options' => [ 98 | 'setValue' => 'prepend', 99 | ], 100 | ], 101 | ], 102 | ], 103 | ], 104 | 'category' => [ 105 | 'config' => [ 106 | 'type' => 'category', 107 | ], 108 | ], 109 | ], 110 | 'types' => [ 111 | '1' => ['showitem' => ' 112 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, 113 | title, description, logo, 114 | --div--;LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_blog.posts, 115 | posts, 116 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, 117 | category, 118 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, 119 | administrator, 120 | --palette--;;paletteHidden, 121 | --palette--;;paletteAccess, 122 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, 123 | --palette--;;paletteLanguage, 124 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended, 125 | '], 126 | ], 127 | 'palettes' => [ 128 | 'paletteHidden' => [ 129 | 'showitem' => 'hidden', 130 | ], 131 | 'paletteAccess' => [ 132 | 'showitem' => ' 133 | starttime, endtime, 134 | --linebreak--, 135 | fe_group', 136 | ], 137 | 'paletteLanguage' => [ 138 | 'showitem' => 'sys_language_uid, l10n_parent', 139 | ], 140 | ], 141 | ]; 142 | -------------------------------------------------------------------------------- /Configuration/TCA/tx_blogexample_domain_model_comment.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment', 6 | 'label' => 'date', 7 | 'label_alt' => 'author', 8 | 'label_alt_force' => true, 9 | 'origUid' => 't3_origuid', 10 | 'tstamp' => 'tstamp', 11 | 'crdate' => 'crdate', 12 | 'delete' => 'deleted', 13 | 'enablecolumns' => [ 14 | 'disabled' => 'hidden', 15 | ], 16 | 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_comment.gif', 17 | ], 18 | 'columns' => [ 19 | 'date' => [ 20 | 'exclude' => true, 21 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.date', 22 | 'config' => [ 23 | 'type' => 'datetime', 24 | 'dbType' => 'datetime', 25 | 'size' => 12, 26 | 'eval' => 'datetime', 27 | 'required' => true, 28 | 'default' => time(), 29 | ], 30 | ], 31 | 'author' => [ 32 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.author', 33 | 'config' => [ 34 | 'type' => 'input', 35 | 'size' => 20, 36 | 'eval' => 'trim', 37 | 'required' => true, 38 | 'max' => 256, 39 | ], 40 | ], 41 | 'email' => [ 42 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.email', 43 | 'config' => [ 44 | 'type' => 'input', 45 | 'size' => 20, 46 | 'eval' => 'trim', 47 | 'required' => true, 48 | 'max' => 256, 49 | ], 50 | ], 51 | 'content' => [ 52 | 'exclude' => true, 53 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_comment.content', 54 | 'config' => [ 55 | 'type' => 'text', 56 | 'rows' => 30, 57 | 'cols' => 80, 58 | ], 59 | ], 60 | 'post' => [ 61 | 'config' => [ 62 | 'type' => 'passthrough', 63 | ], 64 | ], 65 | ], 66 | 'types' => [ 67 | '1' => ['showitem' => 'hidden, date, author, email, content, '], 68 | ], 69 | 'palettes' => [ 70 | '1' => ['showitem' => ''], 71 | ], 72 | ]; 73 | -------------------------------------------------------------------------------- /Configuration/TCA/tx_blogexample_domain_model_info.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info', 6 | 'label' => 'name', 7 | 'tstamp' => 'tstamp', 8 | 'crdate' => 'crdate', 9 | 'versioningWS' => true, 10 | 'transOrigPointerField' => 'l10n_parent', 11 | 'transOrigDiffSourceField' => 'l10n_diffsource', 12 | 'languageField' => 'sys_language_uid', 13 | 'translationSource' => 'l10n_source', 14 | 'origUid' => 't3_origuid', 15 | 'delete' => 'deleted', 16 | 'sortby' => 'sorting', 17 | 'enablecolumns' => [ 18 | 'disabled' => 'hidden', 19 | ], 20 | 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif', 21 | ], 22 | 'columns' => [ 23 | 'name' => [ 24 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info.name', 25 | 'config' => [ 26 | 'type' => 'input', 27 | 'size' => 20, 28 | 'eval' => 'trim', 29 | 'required' => true, 30 | 'max' => 256, 31 | ], 32 | ], 33 | 'bodytext' => [ 34 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_info.bodytext', 35 | 'config' => [ 36 | 'type' => 'text', 37 | 'enableRichtext' => true, 38 | ], 39 | ], 40 | 'post' => [ 41 | 'config' => [ 42 | 'type' => 'passthrough', 43 | ], 44 | ], 45 | ], 46 | 'types' => [ 47 | 0 => ['showitem' => ' 48 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, 49 | name, bodytext, 50 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, 51 | hidden, 52 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, 53 | --palette--;;paletteLanguage, 54 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended, 55 | '], 56 | ], 57 | 'palettes' => [ 58 | 'paletteLanguage' => [ 59 | 'showitem' => 'sys_language_uid, l10n_parent', 60 | ], 61 | ], 62 | ]; 63 | -------------------------------------------------------------------------------- /Configuration/TCA/tx_blogexample_domain_model_person.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person', 6 | 'label' => 'lastname', 7 | 'label_alt' => 'firstname', 8 | 'label_alt_force' => true, 9 | 'tstamp' => 'tstamp', 10 | 'crdate' => 'crdate', 11 | 'versioningWS' => true, 12 | 'transOrigPointerField' => 'l10n_parent', 13 | 'transOrigDiffSourceField' => 'l10n_diffsource', 14 | 'languageField' => 'sys_language_uid', 15 | 'translationSource' => 'l10n_source', 16 | 'origUid' => 't3_origuid', 17 | 'prependAtCopy' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.prependAtCopy', 18 | 'delete' => 'deleted', 19 | 'enablecolumns' => [ 20 | 'disabled' => 'hidden', 21 | ], 22 | 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_person.gif', 23 | ], 24 | 'columns' => [ 25 | 'firstname' => [ 26 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.firstname', 27 | 'config' => [ 28 | 'type' => 'input', 29 | 'size' => 20, 30 | 'eval' => 'trim', 31 | 'required' => true, 32 | 'max' => 256, 33 | ], 34 | ], 35 | 'lastname' => [ 36 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.lastname', 37 | 'config' => [ 38 | 'type' => 'input', 39 | 'size' => 20, 40 | 'eval' => 'trim', 41 | 'required' => true, 42 | 'max' => 256, 43 | ], 44 | ], 45 | 'email' => [ 46 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.email', 47 | 'config' => [ 48 | 'type' => 'input', 49 | 'size' => 20, 50 | 'eval' => 'trim', 51 | 'required' => true, 52 | 'max' => 256, 53 | ], 54 | ], 55 | 'tags' => [ 56 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.tags', 57 | 'config' => [ 58 | 'type' => 'inline', 59 | 'foreign_table' => 'tx_blogexample_domain_model_tag', // needed by Extbase 60 | 'MM' => 'tx_blogexample_domain_model_tag_mm', 61 | 'MM_match_fields' => [ 62 | 'fieldname' => 'tags', 63 | ], 64 | 'appearance' => [ 65 | 'useCombination' => 1, 66 | 'useSortable' => 1, 67 | 'collapseAll' => 1, 68 | 'expandSingle' => 1, 69 | ], 70 | ], 71 | ], 72 | 'tags_special' => [ 73 | 'exclude' => true, 74 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_person.tags_special', 75 | 'config' => [ 76 | 'type' => 'inline', 77 | 'foreign_table' => 'tx_blogexample_domain_model_tag', // needed by Extbase 78 | 'MM' => 'tx_blogexample_domain_model_tag_mm', 79 | 'MM_match_fields' => [ 80 | 'fieldname' => 'tags_special', 81 | ], 82 | 'appearance' => [ 83 | 'useCombination' => 1, 84 | 'useSortable' => 1, 85 | 'collapseAll' => 1, 86 | 'expandSingle' => 1, 87 | ], 88 | ], 89 | ], 90 | ], 91 | 'types' => [ 92 | '1' => ['showitem' => ' 93 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, 94 | firstname, lastname, email, avatar, tags, tags_special, 95 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, 96 | hidden, 97 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, 98 | --palette--;;paletteLanguage, 99 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended, 100 | '], 101 | ], 102 | 'palettes' => [ 103 | 'paletteLanguage' => [ 104 | 'showitem' => 'sys_language_uid, l10n_parent', 105 | ], 106 | ], 107 | ]; 108 | -------------------------------------------------------------------------------- /Configuration/TCA/tx_blogexample_domain_model_post.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post', 6 | 'label' => 'title', 7 | 'label_alt' => 'author', 8 | 'label_alt_force' => true, 9 | 'tstamp' => 'tstamp', 10 | 'crdate' => 'crdate', 11 | 'versioningWS' => true, 12 | 'origUid' => 't3_origuid', 13 | 'transOrigPointerField' => 'l10n_parent', 14 | 'transOrigDiffSourceField' => 'l10n_diffsource', 15 | 'languageField' => 'sys_language_uid', 16 | 'translationSource' => 'l10n_source', 17 | 'delete' => 'deleted', 18 | 'sortby' => 'sorting', 19 | 'enablecolumns' => [ 20 | 'disabled' => 'hidden', 21 | ], 22 | 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_post.gif', 23 | ], 24 | 'interface' => [ 25 | 'maxDBListItems' => 100, 26 | 'maxSingleDBListItems' => 500, 27 | ], 28 | 'columns' => [ 29 | 'blog' => [ 30 | 'exclude' => true, 31 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.blog', 32 | 'config' => [ 33 | 'type' => 'select', 34 | 'renderType' => 'selectSingle', 35 | 'foreign_table' => 'tx_blogexample_domain_model_blog', 36 | 'maxitems' => 1, 37 | ], 38 | ], 39 | 'title' => [ 40 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.title', 41 | 'config' => [ 42 | 'type' => 'input', 43 | 'size' => 20, 44 | 'eval' => 'trim', 45 | 'required' => true, 46 | 'max' => 256, 47 | ], 48 | ], 49 | 'date' => [ 50 | 'exclude' => true, 51 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.date', 52 | 'config' => [ 53 | 'type' => 'datetime', 54 | 'eval' => 'datetime', 55 | ], 56 | ], 57 | 'author' => [ 58 | 'exclude' => true, 59 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.author', 60 | 'config' => [ 61 | 'type' => 'select', 62 | 'renderType' => 'selectSingle', 63 | 'foreign_table' => 'tx_blogexample_domain_model_person', 64 | 'fieldControl' => [ 65 | 'editPopup' => [ 66 | 'disabled' => false, 67 | ], 68 | 'addRecord' => [ 69 | 'disabled' => false, 70 | 'options' => [ 71 | 'setValue' => 'prepend', 72 | ], 73 | ], 74 | ], 75 | ], 76 | ], 77 | 'second_author' => [ 78 | 'exclude' => true, 79 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.second_author', 80 | 'config' => [ 81 | 'type' => 'group', 82 | 'allowed' => 'tx_blogexample_domain_model_person', 83 | 'foreign_table' => 'tx_blogexample_domain_model_person', 84 | 'maxitems' => 1, 85 | 'default' => 0, 86 | 'fieldControl' => [ 87 | 'editPopup' => [ 88 | 'disabled' => false, 89 | ], 90 | 'addRecord' => [ 91 | 'disabled' => false, 92 | ], 93 | 'listModule' => [ 94 | 'disabled' => false, 95 | ], 96 | ], 97 | ], 98 | ], 99 | 'reviewer' => [ 100 | 'exclude' => true, 101 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.reviewer', 102 | 'config' => [ 103 | 'type' => 'select', 104 | 'renderType' => 'selectSingle', 105 | 'foreign_table' => 'tx_blogexample_domain_model_person', 106 | 'fieldControl' => [ 107 | 'editPopup' => [ 108 | 'disabled' => false, 109 | ], 110 | 'addRecord' => [ 111 | 'disabled' => false, 112 | 'options' => [ 113 | 'setValue' => 'prepend', 114 | ], 115 | ], 116 | ], 117 | ], 118 | ], 119 | 'content' => [ 120 | 'exclude' => true, 121 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.content', 122 | 'config' => [ 123 | 'type' => 'text', 124 | 'enableRichtext' => true, 125 | ], 126 | ], 127 | 'tags' => [ 128 | 'exclude' => true, 129 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.tags', 130 | 'config' => [ 131 | 'type' => 'inline', 132 | 'foreign_table' => 'tx_blogexample_domain_model_tag', 133 | 'MM' => 'tx_blogexample_post_tag_mm', 134 | 'appearance' => [ 135 | 'useCombination' => 1, 136 | 'useSortable' => 1, 137 | 'collapseAll' => 1, 138 | 'expandSingle' => 1, 139 | ], 140 | ], 141 | ], 142 | 'comments' => [ 143 | 'exclude' => true, 144 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.comments', 145 | 'config' => [ 146 | 'type' => 'inline', 147 | 'foreign_table' => 'tx_blogexample_domain_model_comment', 148 | 'foreign_field' => 'post', 149 | 'size' => 10, 150 | 'autoSizeMax' => 30, 151 | 'multiple' => 0, 152 | 'appearance' => [ 153 | 'collapseAll' => 1, 154 | 'expandSingle' => 1, 155 | ], 156 | ], 157 | ], 158 | 'related_posts' => [ 159 | 'exclude' => true, 160 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.related', 161 | 'config' => [ 162 | 'type' => 'select', 163 | 'renderType' => 'selectMultipleSideBySide', 164 | 'size' => 10, 165 | 'autoSizeMax' => 30, 166 | 'multiple' => 0, 167 | 'foreign_table' => 'tx_blogexample_domain_model_post', 168 | 'foreign_table_where' => 'AND ###THIS_UID### != tx_blogexample_domain_model_post.uid', 169 | 'MM' => 'tx_blogexample_post_post_mm', 170 | // @see https://forge.typo3.org/issues/98323 171 | // 'MM_opposite_field' => 'related_posts', 172 | ], 173 | ], 174 | 'additional_name' => [ 175 | 'exclude' => true, 176 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.additional_name', 177 | 'config' => [ 178 | 'type' => 'inline', // this will store the info uid in the additional_name field (CSV) 179 | 'foreign_table' => 'tx_blogexample_domain_model_info', 180 | 'minitems' => 0, 181 | 'maxitems' => 1, 182 | ], 183 | ], 184 | 'additional_info' => [ 185 | 'exclude' => true, 186 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.additional_info', 187 | 'config' => [ 188 | 'type' => 'inline', // this will store the post uid in the post field of the info table 189 | 'foreign_table' => 'tx_blogexample_domain_model_info', 190 | 'foreign_field' => 'post', 191 | 'minitems' => 0, 192 | 'maxitems' => 1, 193 | ], 194 | ], 195 | 'additional_comments' => [ 196 | 'exclude' => true, 197 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_post.additional_comments', 198 | 'config' => [ 199 | 'type' => 'inline', // this will store the comments uids in the additional_comments field (CSV) 200 | 'foreign_table' => 'tx_blogexample_domain_model_comment', 201 | 'minitems' => 0, 202 | 'maxitems' => 200, 203 | ], 204 | ], 205 | 'category' => [ 206 | 'config' => [ 207 | 'type' => 'category', 208 | ], 209 | ], 210 | ], 211 | 'types' => [ 212 | '1' => ['showitem' => ' 213 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, 214 | blog, title, date, author, second_author, content, tags, comments, related_posts, additional_name, additional_info, additional_comments, 215 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, 216 | category, 217 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, 218 | hidden, 219 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, 220 | --palette--;;paletteLanguage, 221 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended, 222 | '], 223 | ], 224 | 'palettes' => [ 225 | 'paletteLanguage' => [ 226 | 'showitem' => 'sys_language_uid, l10n_parent', 227 | ], 228 | ], 229 | ]; 230 | -------------------------------------------------------------------------------- /Configuration/TCA/tx_blogexample_domain_model_tag.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_tag', 6 | 'label' => 'name', 7 | 'tstamp' => 'tstamp', 8 | 'crdate' => 'crdate', 9 | 'versioningWS' => true, 10 | 'origUid' => 't3_origuid', 11 | 'transOrigPointerField' => 'l10n_parent', 12 | 'transOrigDiffSourceField' => 'l10n_diffsource', 13 | 'languageField' => 'sys_language_uid', 14 | 'translationSource' => 'l10n_source', 15 | 'delete' => 'deleted', 16 | 'enablecolumns' => [ 17 | 'disabled' => 'hidden', 18 | ], 19 | 'iconfile' => 'EXT:blog_example/Resources/Public/Icons/icon_tx_blogexample_domain_model_tag.gif', 20 | ], 21 | 'columns' => [ 22 | 'name' => [ 23 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_tag.name', 24 | 'config' => [ 25 | 'type' => 'input', 26 | 'size' => 20, 27 | 'eval' => 'trim', 28 | 'required' => true, 29 | 'max' => 256, 30 | ], 31 | ], 32 | 'priority' => [ 33 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_tag.priority', 34 | 'config' => [ 35 | 'type' => 'number', 36 | 'default' => 0, 37 | ], 38 | ], 39 | 'posts' => [ 40 | 'exclude' => true, 41 | 'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xlf:tx_blogexample_domain_model_tag.posts', 42 | 'config' => [ 43 | 'type' => 'select', 44 | 'renderType' => 'selectMultipleSideBySide', 45 | 'size' => 10, 46 | 'minitems' => 0, 47 | 'autoSizeMax' => 30, 48 | 'multiple' => 0, 49 | 'foreign_table' => 'tx_blogexample_domain_model_post', 50 | 'MM' => 'tx_blogexample_post_tag_mm', 51 | 'MM_opposite_field' => 'tags', 52 | ], 53 | ], 54 | ], 55 | 'types' => [ 56 | '1' => ['showitem' => ' 57 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, 58 | name, priority, posts, 59 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, 60 | hidden, 61 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, 62 | --palette--;;paletteLanguage, 63 | --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended, 64 | '], 65 | ], 66 | 'palettes' => [ 67 | 'paletteLanguage' => [ 68 | 'showitem' => 'sys_language_uid, l10n_parent', 69 | ], 70 | ], 71 | ]; 72 | -------------------------------------------------------------------------------- /Configuration/page.tsconfig: -------------------------------------------------------------------------------- 1 | templates.t3docs/blog-example.100 = t3docs/blog-example:Resources/Private/Backend/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: ## Displays this list of targets with descriptions 3 | @echo "The following commands are available:\n" 4 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | .PHONY: install 7 | install: ## Run rector 8 | Build/Scripts/runTests.sh -s composerUpdate 9 | 10 | .PHONY: rector 11 | rector: ## Run rector 12 | Build/Scripts/runTests.sh -s rector 13 | 14 | .PHONY: fix-cs 15 | fix-cs: ## Fix PHP coding styles 16 | Build/Scripts/runTests.sh -s cgl 17 | 18 | .PHONY: fix 19 | fix: rector fix-cs## Run rector and cgl fixes 20 | 21 | .PHONY: phpstan 22 | phpstan: ## Run phpstan tests 23 | Build/Scripts/runTests.sh -s phpstan 24 | 25 | .PHONY: phpstan-baseline 26 | phpstan-baseline: ## Update the phpstan baseline 27 | Build/Scripts/runTests.sh -s phpstanBaseline 28 | 29 | .PHONY: test 30 | test: fix-cs phpstan test-unit test-functional## Run all tests 31 | 32 | .PHONY: test-unit 33 | test-unit: ## Run unit tests 34 | Build/Scripts/runTests.sh -s unit 35 | 36 | .PHONY: test-functional 37 | test-functional: ## Run functional tests 38 | Build/Scripts/runTests.sh -s functional -d mysql 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://poser.pugx.org/t3docs/blog-example/v/stable.svg)](https://extensions.typo3.org/extension/blog_example/) 2 | [![TYPO3 12](https://img.shields.io/badge/TYPO3-13-orange.svg?style=flat-square)](https://get.typo3.org/version/13) 3 | [![TYPO3 12](https://img.shields.io/badge/TYPO3-12-orange.svg?style=flat-square)](https://get.typo3.org/version/12) 4 | [![Total Downloads](https://poser.pugx.org/t3docs/blog-example/d/total.svg)](https://packagist.org/packages/t3docs/blog-example) 5 | [![Monthly Downloads](https://poser.pugx.org/t3docs/blog-example/d/monthly)](https://packagist.org/packages/t3docs/blog-example) 6 | ![Build Status](https://github.com/TYPO3-Documentation/blog_example/actions/workflows/tests.yml/badge.svg) 7 | 8 | # TYPO3 extension ``blog_example`` 9 | 10 | **Installation:** Can be installed via Composer: 11 | ``composer req t3docs/blog-example`` 12 | 13 | This example provides the code examples for [Extbase reference] (https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ExtensionArchitecture/Extbase/Reference/Index.html#extbase-reference) 14 | of the TYPO3 project. 15 | 16 | The code examples are automatically extracted with the TYPO3 documentation 17 | code-snippet tool (https://github.com/TYPO3-Documentation/t3docs-codesnippets) 18 | 19 | After changes to the code the snippets in the TYPO3-Reference-CoreApi 20 | have to be regenerated. 21 | 22 | ``` 23 | ddev exec vendor/bin/typo3 restructured_api_tools:php_domain public/fileadmin/TYPO3CMS-Reference-CoreApi/Documentation/CodeSnippets/ 24 | ``` 25 | 26 | It was originally written by Sebastian Kurfuerst and Jochen Rau (Thanks!) and 27 | adjusted over time to reflect current development in the TYPO3 project. 28 | 29 | | | URL | 30 | |------------------|------------------------------------------------------| 31 | | **Repository:** | https://github.com/TYPO3-Documentation/blog_example | 32 | | **TER:** | https://extensions.typo3.org/extension/blog_example/ | 33 | 34 | # Running tests 35 | 36 | Please see [CONTRIBUTING.md](CONTRIBUTING.md) 37 | 38 | 39 | # Tagging and releasing 40 | 41 | [packagist.org](https://packagist.org/packages/t3docs/blog-example) is enabled via the casual GitHub hook. 42 | TER releases are created by the "publish.yml" GitHub workflow when tagging versions. 43 | The commit message of the tagged commit is used as TER upload comment. 44 | -------------------------------------------------------------------------------- /Resources/Private/Backend/Templates/Index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Here is a list of blogs:

12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 32 | 58 | 59 | 60 | 61 | 62 |
BlogDescription 
25 | 26 | {blog.title} 27 | ({blog.posts -> f:count()}) 28 | 30 | {blog.description} 31 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 53 | 54 | 55 | 56 |
57 |
63 |
64 |

65 | 66 | Delete all Blogs [!!!] 67 | 68 |

69 |
70 | 71 |

72 | Create example data 73 |

74 |
75 |
76 |
77 | 78 | -------------------------------------------------------------------------------- /Resources/Private/Backend/Templates/ShowAllComments.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 |
10 |

List of all comments

11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | {comment.author} posted on 19 | {comment.date} 20 | 21 |
22 |
23 | {comment.content} 24 |
25 |
26 |
27 |
28 |
29 | 30 |

This blog currently doesn't contain any comments.

31 |
32 |
33 |
34 |
35 | 36 | -------------------------------------------------------------------------------- /Resources/Private/Backend/Templates/ShowBlog.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 |

Posts of Blog "{blog.title}"

10 |

{blog.description}

11 |
12 | 13 | 14 |
    15 | 16 |
  • 17 |

    18 | 19 | 20 | {post.title} 21 | 22 |

    23 |

    24 | {post.content} 25 |

    26 |
  • 27 |
    28 |
29 |
30 | 31 |

This blog currently doesn't contain any posts.

32 |
33 |
34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /Resources/Private/Backend/Templates/ShowPost.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | {post.date} 12 |

13 | {post.title} 14 |

15 |

16 | {post.content} 17 |

18 |

By: {post.author.fullName}

19 | 20 | 21 |
Related posts:
22 | 29 |
30 | 33 |
34 |
35 |

Comments

36 | 37 |
38 |
39 | 40 | 41 | {comment.author} posted on 42 | {comment.date} 43 | 44 |
45 |
46 | {comment.content} 47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 | -------------------------------------------------------------------------------- /Resources/Private/Language/Module/locallang_mod.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Blogs 8 | 9 | 10 | View/Modify Blogs 11 | 12 | 13 | Blogs 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Blog list 8 | 9 | 10 | All comments 11 | 12 | 13 | back 14 | 15 | 16 | cancel 17 | 18 | 19 | submit 20 | 21 | 22 | Log in with a FE user that belongs to group #%d (configurable by settings.editorUsergroupUid) in order to add/modify blogs and posts in FE mode! 23 | 24 | 25 | published on 26 | 27 | 28 | by 29 | 30 | 31 | posted on 32 | 33 | 34 | Welcome to the Blog Example! 35 | 36 | 37 | This extension serves as a demonstration for basic development techniques used in Extbase applications. 38 | 39 | 40 | Here is a list of %d blogs 41 | 42 | 43 | %d posts 44 | 45 | 46 | There are currently no blogs 47 | 48 | 49 | Edit this blog 50 | 51 | 52 | Delete this blog 53 | 54 | 55 | Create your first blog 56 | 57 | 58 | Create another blog 59 | 60 | 61 | Create sample data 62 | 63 | 64 | Delete all blogs 65 | 66 | 67 | Are you sure that you want to delete all blogs? 68 | 69 | 70 | Edit blog "%s" 71 | 72 | 73 | Edit the information about your blog below 74 | 75 | 76 | Create new blog 77 | 78 | 79 | Edit the information about the new blog below 80 | 81 | 82 | Welcome to the Blog "%s" 83 | 84 | 85 | If you have any questions, feel free to contact the administrator %s. 86 | 87 | 88 | There is currently no administrator assigned to this Blog 89 | 90 | 91 | This blog contains %d posts 92 | 93 | 94 | This blog contains %d posts tagged with "%s" 95 | 96 | 97 | There are currently no posts in this blog 98 | 99 | 100 | Create your first post 101 | 102 | 103 | Create another post 104 | 105 | 106 | Show all posts of this blog 107 | 108 | 109 | Edit this post 110 | 111 | 112 | Delete this post 113 | 114 | 115 | read more 116 | 117 | 118 | related posts 119 | 120 | 121 | Comments 122 | 123 | 124 | Add your own 125 | 126 | 127 | Edit post "%s" 128 | 129 | 130 | Create new post 131 | 132 | 133 | Show posts as plaintext 134 | 135 | 136 | Delete this comment 137 | 138 | 139 | title 140 | 141 | 142 | description 143 | 144 | 145 | administrator 146 | 147 | 148 | content 149 | 150 | 151 | author 152 | 153 | 154 | email 155 | 156 | 157 | message 158 | 159 | 160 | related posts 161 | 162 | 163 | Blog added 164 | 165 | 166 | A new blog was created 167 | 168 | 169 | Blog updated 170 | 171 | 172 | The blog was updated 173 | 174 | 175 | Blog deleted 176 | 177 | 178 | The blog has been removed 179 | 180 | 181 | Sample data created 182 | 183 | 184 | Some exemplary blogs and posts have been created 185 | 186 | 187 | Post added 188 | 189 | 190 | A new post was created 191 | 192 | 193 | Post updated 194 | 195 | 196 | The blog post was updated 197 | 198 | 199 | Post deleted 200 | 201 | 202 | The blog post has been removed 203 | 204 | 205 | Comment added 206 | 207 | 208 | Your comment has been created 209 | 210 | 211 | Comment deleted 212 | 213 | 214 | The comment has been removed 215 | 216 | 217 | Comments deleted 218 | 219 | 220 | All comments of this post have been removed 221 | 222 | 223 | Y-m-d H:i\h 224 | 225 | 226 | Y-m-d 227 | 228 | 229 | Following errors occurred 230 | 231 | 232 | Could not update blog 233 | 234 | 235 | Could not create blog 236 | 237 | 238 | "Extbase" can't be used as a blog title 239 | 240 | 241 | Could not update post 242 | 243 | 244 | Could not create post 245 | 246 | 247 | Could not create comment 248 | 249 | 250 | The given <strong>%s</strong> is either too long or too short 251 | 252 | 253 | The given <strong>%s</strong> is too short 254 | 255 | 256 | The given <strong>%s</strong> is too long 257 | 258 | 259 | The given <strong>%s</strong> is not a valid email address 260 | 261 | 262 | The <strong>%s</strong> must not be empty 263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_db.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Blog 8 | 9 | 10 | 11 | Blog 12 | 13 | 14 | Blog title 15 | 16 | 17 | Short description 18 | 19 | 22 | 23 | Posts 24 | 25 | 26 | Administrator 27 | 28 | 29 | This frontend user may edit the blog and all its posts. 30 | 31 | 32 | Post 33 | 34 | 35 | Related to 36 | 37 | 38 | Title 39 | 40 | 41 | Date 42 | 43 | 44 | Author 45 | 46 | 47 | Content 48 | 49 | 50 | Votes 51 | 52 | 53 | Published 54 | 55 | 56 | Tags 57 | 58 | 59 | Comments 60 | 61 | 62 | Related posts 63 | 64 | 65 | Second author 66 | 67 | 68 | Additional name 69 | 70 | 71 | Additional info 72 | 73 | 74 | Additional comments 75 | 76 | 77 | Person 78 | 79 | 80 | Firstname 81 | 82 | 83 | Lastname 84 | 85 | 86 | E-Mail 87 | 88 | 89 | Avatar 90 | 91 | 92 | Comment 93 | 94 | 95 | Date 96 | 97 | 98 | Author 99 | 100 | 101 | Email 102 | 103 | 104 | Content 105 | 106 | 107 | Tag 108 | 109 | 110 | Name 111 | 112 | 113 | Priority 114 | 115 | 116 | Related posts 117 | 118 | 119 | Blog Admin (BlogExample) 120 | 121 | 122 | Bloginfo 123 | 124 | 125 | Name 126 | 127 | 128 | Text 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /Resources/Private/Layouts/Default.html: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 | 6 | 7 |
8 |
9 |
10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /Resources/Private/Partials/BlogForm.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 |
6 |
7 | 10 |
11 |
12 | 13 |
14 |
15 | 18 |
19 |
20 |