A website in memory of the famous Bulgarian poet Yosif Petrov containing his works; press articles, videos and photographs of him, as well as other useful information about him.
Preview: https://yosifpetrov.com
- PHP 7.1 to 7.4
- MySQL 5.7
- Doctrine ORM 2.9+
- MVC
- Models
- Controllers
- Views (Smarty 3.1+)
- PHPMailer library
The general recommendation that I stumbled upon while researching was NOT to include the /vendor
folder in the repository, but rather let composer install all dependencies.
Therefore all custom Doctrine files are to be found in the /src/models/doctrine
folder. Still, Doctrine doesn’t seem to fancy such harsh structural changes, so all classes had to be manually loaded using composer’s autoload
property (at least I couldn’t find a simpler way to do it).
NB! Traits and Interfaces need to be loaded manually beforehand so all other classes which depend on them could actually work.
Doctrine’s mapping is done using xml
files located in /src/models/doctrine/xml
instead of PHP annotations – I prefer to keep them separated so as to improve readability.
The project uses some sort of a simplified MVC design pattern inspired by this article:
- Models are found in the
/src/models
folder - Controllers are all php files in the
/src/controllers
folder and subfolders - Views are located in
/src/views
Routes were previously declared in the root .htaccess
file which had several downsides:
- it was not flexible
- it was not scalable
- it was not pretty
- the app had multiple entry-points which is confusing and doesn’t correspond with the DRY principle
- parameters were sent using the
$_GET
super global which meant they could be overridden
Instead, all requests are now being sent to the public/index.php
file where a newly introduced Router
class tries to match the current URL to any of the pre-declared routes and actually calls up the relevant controllers and their actions. Another benefit is the URL generation method which saves a lot of headache in case a section gets renamed/moved: simply changing the route declaration takes care of it all.
Apart from the single classes (such as Gallery
or PressArticle
), the essential part of this project are the poet’s works. Their content is being shared between the following classes:
- Book: all published books of the poet
- Poem: a repository of all poems (some poems belong to more than one book)
- BookContent: connecting Books with Poems.
Initially, the relation between Book and Poem was supposed to be of many-to-many
type. However, such relations can only hold information about the foreign keys of the interconnected classes, but we need to be able to reorder or deactivate certain poems for certain books. Therefore, having other columns in the connecting table turns it into a class of its own – BookContent, so two one-to-many
relations are needed instead:
- Book → one-to-many → BookContent
- Poem → one-to-many → BookContent
Furthermore, most classes can have comments. Comments share the same structure and can consequently be stored in one and the same table using a separate column indicator such as entity_class
. Unfortunately, Doctrine doesn’t offer a simple way to implement one-to-many
unidirectional associations with additional static JOIN columns. A possible solution could be to have separate one-to-many
Class → Comment associations with separate tables which seems kind of clumsy.
As a result, comments get stored in the same table, but as a trade-off they must be loaded separately using CommentRepository and passing the main entity as parameter. Not the best solution, but it works for now.
NB! Classes which can be commented on should implement the CommentableInterface.
All controllers extend either the CommentableController
class (which in turn extends AppController
) or the AppController
itself. Both are abstract base classes and hold methods and properties which can be used across all controllers. For instance, the array property $globalModels
in AppController
stores all Doctrine models which are needed on most pages (such as loading data for the navigation). They get initialized on Controller load.
NB! Models which are to be used only per-controller are declared in the
$models
array property of said controller.
Each request loads up only its corresponding controller (if any). Base controllers are loaded automatically using the autoload
declaration in composer.json
. The downside to that is that autoload
cannot “see” newly introduced base controllers: composer dump-autoload
needs to be executed first.
To improve readability in the Views part of the MVC architectural pattern, all templates use Smarty PHP template engine.
NB! Since Smarty requires PHP version no greater than 7.4, the same applies to the current project, too.
Currently data is mostly read from the database. The only classes which support dynamical creation of new records are Comment and ContactMessage, both of which have their own event subscribers which listen for two events:
- prePersist – to do some basic validation
- postPersist – to send an email notification to the administrators about new messages using the PHPMailer library (administrators’ email addresses are stored in a separate table,
configs
).
SMTP settings are stored in the
src/core/SMTPConfig.php
file having separate declarations for development and production environments. If there’s a mail failure, the end user does not get notified about it – instead, the error gets logged using a simple error loggng class – Logger.
There’s a simple custom Logger
class which takes care of logging major (but not fatal) errors which may occur during execution, such as:
- Potential database failure
- Page not found (like trying to access an article marked as inactive)
- AJAX errors
- PHPMailer errors
Error log files have a maximum file size of 10MB. Once this size is reached, a new log file gets created in the same folder having a consecutive number as a suffix preceding the extension: errors.log
, errors1.log
, errors2.log
, etc.
Previosly the website has used exclusively URLs written in Cyrillic script. This is no longer the case (copy-pasted URLs with cyrillic characters in them are not the prettiest of sights), so 301 redirects
must be set up for the sake of search engine rankings, but also for the end users’ benefit.
Initially only requests starting with a cyrillic letter were routed via the root .htaccess
to the redirector which was not that flexible. Instead, Redirector
got turned into a class of its own which gets loaded before the current request gets processed. This allows for almost all sorts of redirects.
When called, the Redirector loads up an array with all old addresses and their respective relocations. If the requested URL is present in this array, a 301 redirect to the new address is issued; otherwise the request continues being processed the usual way.
mobiCMS Captcha is used as a simple spam prevention tool. A captcha code gets generated on page load or mouse click which then gets stored in a session variable. Its validity is verified in the prePersist
method of both Comment-
and ContactMessageSubscriber
-s.
- HTML5
- LESS/CSS3
- jQuery 3.4.1
- Magnific Popup 1.1.0 (jQuery library)
- Swipe 2.3.0 (jQuery library)
The website is responsive with the emphasis being placed on desktop and mobile devices (not so much on tablets). The navigation bar’s position is fixed on scroll, and there’s a hamburger button in the right corner on mobile. All images which are part of the content can be zoomed-in using the Magnific Popup plugin, while the Swipe plugin is used to browse between images in the Gallery section (lazy-loading has been implemented to reduce server response time).
To improve the overall UX some interactions between the end-user and the client rely on AJAX. For instance, poems within books are loaded asynchroniously (covering both click
and popstate
events).
Comments and contact messages are also processed without a page reload, while providing the user with realtime feedback about the request, such as potential validation errors or successful record creation.
Videos are embeded using the HTML 5 <video>
tag. For wider video support across platforms two video sources are offered:
- mp4
- webm
ffmpeg
was used to deinterlace and trim the source video, as well as to re-encode it to cover for the available formats.
1. Create database
2. Edit database configuration in `src/core/DatabaseConfig.php`
3. Execute SQL dump located in `src/ypetrov-database-structure-dump.sql`
4. Install dependencies using `composer install`
NB! Database content is NOT featured in this GitHub project.
A Content Management System (CMS) needs to be implemented at some point. This includes a simple user authentication method, a WYSIWYG text editor and other UI tools which will allow for the content to be altered, edited and rearranged in a user-friendly way.
Additionally, sections may be subsequently added, restructured or removed from the website as more information gets introduced to the project.