Infocurci
* Claudio Curci  /  Php developer
'
* Claudio Curci  /  Php developer

5. Lezione: Un confronto tra il Php di oggi e quello di ieri (Joomla! 1.5)


Mettiamo a confronto quello che, molti anni fa, era considerato lo stato dell'arte dei CMS, Joomla 1.5, con le best practice che abbiamo visto nelle lezioni precedenti.

Com'era strutturato Joomla 1.5?

Procediamo oggi con un simpatico esperimento, andiamo ad analizzare quello che veniva fatto diversi anni fa in uno dei più popolari CMS dell'epoca, ovvero Joomla 1.5. Questo è il codice della funzione save() che si occupava di salvare il contenuto di un argomento nel file controller.php del componente com_content.

<?php
function save()
            {
                // Check for request forgeries
                JRequest::checkToken() or jexit( 'Invalid Token' );

                // Initialize variables
                $db         = & JFactory::getDBO();
                $user       = & JFactory::getUser();
                $task       = JRequest::getVar('task', null, 'default', 'cmd');

                // Make sure you are logged in and have the necessary access rights
                if ($user->get('gid') < 19) {
                    JError::raiseError( 403, JText::_('ALERTNOTAUTH') );
                    return;
                }

                // Create a user access object for the user
                $access                 = new stdClass();
                $access->canEdit        = $user->authorize('com_content', 'edit', 'content', 'all');
                $access->canEditOwn     = $user->authorize('com_content', 'edit', 'content', 'own');
                $access->canPublish     = $user->authorize('com_content', 'publish', 'content', 'all');

                if (!($access->canEdit || $access->canEditOwn)) {
                    JError::raiseError( 403, JText::_("ALERTNOTAUTH") );
                }

                //get data from the request
                $model = $this->getModel('article');

                //get data from request
                $post = JRequest::get('post');
                $post['text'] = JRequest::getVar('text', '', 'post', 'string', JREQUEST_ALLOWRAW);

                //preform access checks
                $isNew = ((int) $post['id'] < 1);

                if ($model->store($post)) {
                    $msg = JText::_( 'Article Saved' );

                    if($isNew) {
                        $post['id'] = (int) $model->get('id');
                    }
                } else {
                    $msg = JText::_( 'Error Saving Article' );
                    JError::raiseError( 500, $model->getError() );
                }

                // manage frontpage items
                //TODO : Move this into a frontpage model
                require_once (JPATH_ADMINISTRATOR.DS.'components'.DS.'com_frontpage'.DS.'tables'.DS.'frontpage.php');
                $fp = new TableFrontPage($db);

                if (JRequest::getVar('frontpage', false, '', 'boolean'))
                {
                    // toggles go to first place
                    if (!$fp->load($post['id']))
                    {
                        // new entry
                        $query = 'INSERT INTO #__content_frontpage' .
                                ' VALUES ( '.(int) $post['id'].', 1 )';
                        $db->setQuery($query);
                        if (!$db->query()) {
                            JError::raiseError( 500, $db->stderr());
                        }
                        $fp->ordering = 1;
                    }
                }
                else
                {
                    // no frontpage mask
                    if (!$fp->delete($post['id'])) {
                        $msg .= $fp->stderr();
                    }
                    $fp->ordering = 0;
                }
                $fp->reorder();

                $model->checkin();

                // gets section name of item
                $query = 'SELECT s.title' .
                        ' FROM #__sections AS s' .
                        ' WHERE s.scope = "content"' .
                        ' AND s.id = ' . (int) $post['sectionid'];
                $db->setQuery($query);
                // gets category name of item
                $section = $db->loadResult();

                $query = 'SELECT c.title' .
                        ' FROM #__categories AS c' .
                        ' WHERE c.id = ' . (int) $post['catid'];
                $db->setQuery($query);
                $category = $db->loadResult();

                if ($isNew)
                {
                    // messaging for new items
                    require_once (JPATH_ADMINISTRATOR.DS.'components'.DS.'com_messages'.DS.'tables'.DS.'message.php');

                    // load language for messaging
                    $lang =& JFactory::getLanguage();
                    $lang->load('com_messages');

                    $query = 'SELECT id' .
                            ' FROM #__users' .
                            ' WHERE sendEmail = 1';
                    $db->setQuery($query);
                    $users = $db->loadResultArray();
                    foreach ($users as $user_id)
                    {
                        $msg = new TableMessage($db);
                        $msg->send($user->get('id'), $user_id, JText::_('New Item'), JText::sprintf('ON_NEW_CONTENT', $user->get('username'), $post['title'], $section, $category));
                    }
                } else {
                    // If the article isn't new, then we need to clean the cache so that our changes appear realtime :)
                    $cache = &JFactory::getCache('com_content');
                    $cache->clean();
                }

                if ($access->canPublish)
                {
                    // Publishers, admins, etc just get the stock msg
                    $msg = JText::_('Item successfully saved.');
                }
                else
                {
                    $msg = $isNew ? JText::_('THANK_SUB') : JText::_('Item successfully saved.');
                }
                
                $referer = JRequest::getString('ret',  base64_encode(JURI::base()), 'get');
                $referer = base64_decode($referer);
                if (!JURI::isInternal($referer)) {
                    $referer = '';
                }
                $this->setRedirect($referer, $msg);     
            }

Come vedete, all'epoca i controller erano un pò i "camerieri in sala", quelli che in teoria dovrebbero fare da raccordo tra cucina e clienti ma che, nella pratica di tutti i giorni, si occupano anche di altro (come guarnire le porzioni dei dolci prima di servirle, condire le insalate e predisporre i conti per la cassa).

In Joomla abbiamo un esempio di cosa oggi non andrebbe mai fatto, viste le evoluzioni che ha offerto nel frattempo Php. Se all'epoca era normale trovare controller con oltre 350 righe di codice e metodi, come quello appena visto, lunghi più di 130 righe, oggi le best practice guidano gli sviluppatori verso controller minimali e deleghe ad altri tipi di componenti, come Repository per l'accesso ai dati e Service Layer per la logica di business.

Il controller/metodo oggi assumerebbe una forma più simile a questa:

<?php
final class ArticleController
            {
                public function __construct(
                    private ArticleService $service
                ) {}

                public function save(Request $request): Response
                {
                    $this->service->save(
                        new SaveArticleDTO(
                            id: $request->input('id'),
                            title: $request->input('title'),
                            text: $request->input('text'),
                            sectionId: $request->input('sectionid'),
                            categoryId: $request->input('catid'),
                            isFrontPage: $request->boolean('frontpage'),
                            userId: $request->user()->id
                        )
                    );

                    return new RedirectResponse(
                        $request->input('ret', '/'),
                        'Article saved successfully'
                    );
                }
            }

dove il codice si limita a 'orchestrare' al meglio le chiamate ad altri componenti, che svolgeranno il lavoro vero e proprio. Avremo un Service Layer, in questo caso dedicato agli articoli:

<?php
final class ArticleService
            {
                public function __construct(
                    private ArticleRepository $articles,
                    private FrontPageRepository $frontPage,
                    private UserRepository $users,
                    private EventDispatcher $events
                ) {}

                public function save(SaveArticleDTO $dto): void
                {
                    $isNew = $dto->id === null;

                    $article = $this->articles->save($dto);

                    $this->handleFrontPage($article->id, $dto->isFrontPage);

                    if ($isNew) {
                        $this->notifyNewArticle($article, $dto->userId);
                    } else {
                        $this->articles->clearCache();
                    }
                }
            }

anche la logica del frontpage (vale a dire il codice che pubblica gli articoli nella prima pagina, all'epoca di Joomla 1.5 era una feature essenziale) verrebbe gestita da un altro service con un metodo come questo:

<?php
$this->frontPageService->handleFrontPage(...);

e nel service:

<?php
public function handleFrontPage(int $articleId, bool $isFrontPage): void
            {
                if ($isFrontPage) {
                    $this->frontPage->add($articleId);
                } else {
                    $this->frontPage->remove($articleId);
                }
            }

Una parte delle responsabilità che un tempo appartenevano ai Model viene oggi affidata ai Repository e i dati vengono trasmessi attraverso il DTO. Vediamo meglio questi aspetti.

Che differenza c'è tra un service e un helper? Non potrei usare delle funzioni, organizzate in più helper?

Sì, certo, e funzionerà comunque bene, visto che la differenza la fanno le istruzioni contenute dentro le funzioni. Però lavorare in questa maniera significa derogare dalle best practice di php (ed in generale della moderna programmazione ad oggetti) e pagarne prima o poi le conseguenze, quantomeno su progetti in evoluzione.

C'è comunque una bella differenza tra un service e un helper. Gli helper sono generalemente insiemi di funzioni, prive quindi di uno stato condiviso. Sono, in un certo senso, l'evoluzione dei vecchi "require('functions.inc.php')". Un service è un oggetto vero e proprio, che può incapsulare logiche di business e coordinare altre dipendenze.

Se i service sono cosi importanti, non potrei farne a meno e scrivere le relative funzioni direttamente dentro al controller?

Un controller non nasce per gestire un intero software o una parte di esso. Nasce per gestire la comunicazione http. E' una sorta di ponte che, 'catturata' una url, riceve la richiesta HTTP, estrae i dati necessari e delega il lavoro agli altri componenti. Se mettiamo tutto quello che possiamo dentro un controller, lo snaturiamo e perdiamo opportunità. Ad esempio quella di usare lo stesso codice da CLI, oppure da una coda cron, o semplicemente di eseguire test senza passare per HTTP. Ricordate sempre le parole di Stroustrup "pensate i programmi a librerie, e innalzate il livello d'astrazione".

Che differenza c'è tra un Model "vecchia maniera" e un repository? Non si occupano entrambi di eseguire query, aggiornare, estrarre, restituire dati?

In Joomla 1.5 avevamo com_content/models/article.php con i metodi get(), getArticle(), store() e oltre 600 righe di codice. E questo la dice lunga. Il Model del vecchio MVC era una sorta di factotum che interagiva con il db ma si occupava anche di validare dati, applicare regole di business o addirittura di cache. Inoltre interagiva direttamente con le tabelle, e questo implicava che doveva conoscerne la struttura ed era di solito scritto per una sola fonte di dati.

E qui torniamo a Stroustrup: i repository moderni astraggono il meccanismo di persistenza dei dati. Nessuna logica di business ma lo stretto indispensabile, ovvero recuperare un articolo dal suo ID, salvarlo, eliminarlo o eseguire ricerche. La logica di business è delegata ad altro, ad esempio ai service, mentre il repository si occupa dei dati e, oltre a sollevare il model da responsabilità di logiche di verifica e controllo (come ad esempio la presenza di campi obbligatori), consente di dichiarare un'interfaccia e delegare l'effettiva implementazione alle classi derivate. In questo modo l'applicativo può interfacciarsi con il Repository a prescindere dal tipo di sorgente dati (che potrebbe essere MySQL come Redis, Cache, Postgre...).

Cosa é un DTO?

DTO significa Data Transfer Object, e il termine Transfer è quello essenziale. E' un oggetto che si occupa di trasportare dei dati da un punto all'altro dell'applicazione. Classico esempio, l'esigenza di portare i dati inviati da un utente attraverso POST al service che si occupa di salvarli. E' vero che $_POST in php è un array super globale, e che in teoria basterebbe questa istruzione

<?php
$articleService->save($_POST);

Questo approccio è limitato. Non abbiamo alcuna verifica sui tipi o sui campi obbligatori, cosi come un semplice errore di battitura mentre digitiamo le chiavi (come 'titel' anzichè 'title') non provoca errori evidenti.
Con un DTO invece possiamo stabilire in maniera esplicita i dati

<?php
final readonly class SaveArticleDTO
            {
                public function __construct
                (
                    public string $title
                    public string $text,
                    public int $categoryId,
                    public bool $published
                ) 
                {}
            }

in questo modo stabiliamo un "contratto" di dati in modo da passare valori congrui e coerenti. Questo anche perchè in caso di tipo errato, Php lancerà un TypeError (con strict_types=1), cosa che non accadrebbe in caso di array.

Un DTO non nasce per sostituire gli array ma piuttosto uno strumento per progettare codice, in particolare API, più chiare e robuste. Avrete probabilmente notato che la classe è dichiarata final e readonly. È una scelta voluta: un DTO non rappresenta un oggetto del dominio, ma un semplice contratto tra due componenti dell'applicazione. Deve limitarsi a trasportare dati, sfruttando la tipizzazione del linguaggio per intercettare eventuali errori il prima possibile.

Dal 1997, il Php a Roma!

Claudio Curci "La vera efficienza si trova nella serenità." (Henry David Thoreau)

"Guardati dalla sterilità di una vita sempre indaffarata." (Socrate)

Da quasi trent'anni mi dedico alla programmazione, con oltre 20 anni di esperienza come freelance. Certificato booking.com. Mi occupo di tutto quello che gravita attorno al Php: software custom, Magento, Woocommerce, Prestashop, Wordpress, Joomla!, Moodle.
Infocurci Questo sito rispetta gli utenti:
  • Non ospita nessuna pubblicità
  • Non utilizza cookie e non profila nulla
  • Non spreca risorse energetiche per asettiche illustrazioni AI
Navigate serenamente, siete i benvenuti.

Ambienti / piattaforme di sviluppo

Amazon Booking.com CodeIgniter Joomla Magento Moodle Kelkoo Kigo PayPal Symfony Wordpress Airbnb FormaLms Laravel Prestashop Shopify Whatsapp