All notable changes to this project will be documented in this file.
Patch che corregge AdapterMysql::close(), mai funzionante, e la riconnessione dopo la chiusura, e che rifiuta gli ID non validi restituiti dal database dopo un inserimento invece di assegnarli all'entità.
Orm/Adapters/AdapterMysql::close() — errore a ogni chiusura
close() eseguiva unset(self::$connection), ma PHP non consente l'unset di una proprietà statica: con una connessione aperta il metodo sollevava sempre Error: Attempt to unset static property e la connessione non veniva chiusa. Ora close() azzera la connessione.
Orm/BaseClasses/BaseAdapter::ensureConnected() — nessuna riconnessione dopo close()
La connessione è statica e condivisa da tutti gli adapter, mentre isConnected è una proprietà di ogni singola istanza. Dopo una chiusura isConnected restava true, sia per l'adapter che aveva chiuso sia per gli altri, e ensureConnected() non riapriva la connessione: le query successive restituivano null e le esecuzioni false senza sollevare errori. ensureConnected() verifica ora anche che la connessione condivisa sia aperta, e close() azzera isConnected. Un adapter si riconnette quindi alla prima operazione successiva a una chiusura, anche se la chiusura è stata eseguita da un altro adapter.
Orm/Adapters/AdapterMysql::getLastErrorCode() — TypeError prima della prima operazione
PDO::errorCode() restituisce null se sulla connessione non è ancora stata eseguita alcuna operazione, e il metodo, dichiarato string, sollevava un TypeError. Il caso non si presenta con le connessioni aperte da connect(), che esegue subito SET names, ma con una connessione impostata tramite setConnection(). Il metodo restituisce ora '' in questo caso, e '-1' come stringa in assenza di connessione al posto dell'intero -1, prima convertito implicitamente.
Orm/Adapters/AdapterMysql::lastInsertIdToDelegateAdapter() — ID non validi assegnati all'entità
DataMapper::insert() esclude la chiave primaria dall'INSERT e assegna all'entità l'ID restituito da lastInsertId(), che non veniva verificato:
AUTO_INCREMENT MySQL restituisce 0. L'entità riceveva id = 0, e al salvataggio successivo DataMapper::save() la considerava nuova e inseriva una seconda riga;-1. L'entità riceveva id = -1, e al salvataggio successivo l'UPDATE non trovava alcuna riga ma risultava riuscito: le modifiche andavano perse senza errori;false, convertito in 0;PHP_INT_MAX (BIGINT UNSIGNED) sollevava un TypeError non descrittivo.
Il metodo solleva ora un'AdapterException se l'ID non è un intero positivo o se non c'è connessione, come già avviene quando l'INSERT stesso fallisce.
File modificati:
Orm/BaseClasses/BaseAdapter.php: ensureConnected() riapre la connessione anche quando quella condivisa è stata chiusaOrm/Adapters/AdapterMysql.php: close() azzera la connessione e isConnected senza unset(); getLastErrorCode() restituisce '-1' senza connessione e '' se PDO::errorCode() restituisce null; lastInsertIdToDelegateAdapter() valida l'ID con FILTER_VALIDATE_INT e solleva AdapterException senza connessione o con un ID non positivoTests/Orm/Adapters/AdapterMysqlTest.php: aggiunti testCloseReleasesConnection(), testOperationAfterCloseReconnects(), testOperationAfterCloseByAnotherAdapterReconnects(), testGetLastErrorCodeBeforeAnyOperation(), testLastInsertIdWithoutConnection() e testInvalidLastInsertId(), che falliscono senza le correzioni; i test che simulavano l'assenza di connessione con setConnection(null), ora seguito da una riconnessione, usano un adapter il cui connect() non apre alcuna connessioneclose() la prima operazione di un adapter riapre la connessione, invece di restituire null o false.lastInsertId() solleva AdapterException invece di restituire -1 senza connessione o un ID non positivo. Il salvataggio di un'entità in una tabella senza AUTO_INCREMENT fallisce ora con un'eccezione; prima riusciva, ma assegnava all'entità id = 0 e duplicava la riga al salvataggio successivo.
Minor release dedicata a Session. L'indirizzo IP può essere escluso dal token anti-dirottamento, per le applicazioni dietro un reverse proxy o con utenti che cambiano rete durante la navigazione, e la lettura di chiavi annidate inesistenti non scrive più nella sessione.
Core/HelperClasses/Session — indirizzo IP nel token anti-dirottamento configurabile
Il token anti-dirottamento, verificato da Session::isValidSession(), è l'impronta di User-Agent e indirizzo IP del client. L'indirizzo IP non sempre identifica il client: i dispositivi mobili che cambiano rete e i client dual-stack che passano da IPv4 a IPv6 cambiano indirizzo durante la navigazione, e isValidSession() restituisce false per un utente legittimo; dietro un reverse proxy, un load balancer o una CDN REMOTE_ADDR è invece l'indirizzo del proxy, uguale per tutti gli utenti, e non aggiunge protezione. Con la nuova costante SESSION_TOKEN_INCLUDES_REMOTE_ADDRESS impostata a false l'impronta usa il solo User-Agent. Il default true mantiene il comportamento precedente.
Config/config.php: nuova costante SESSION_TOKEN_INCLUDES_REMOTE_ADDRESS (default true), esposta come Config::$sessionTokenIncludesRemoteAddressCore/HelperClasses/Session::hasItem(), getItem() e unsetItem() — chiavi annidate inesistenti create in sessione
Con una chiave annidata, come cart[items][0], i tre metodi passavano $_SESSION['cart'] per riferimento ai rispettivi metodi ricorsivi, e PHP crea gli elementi inesistenti passati per riferimento: un semplice controllo come Session::hasItem('cart[items][0]') su una sessione vuota restituiva false ma lasciava in sessione ['cart' => ['items' => null]], e un successivo Session::hasItem('cart') restituiva true. I metodi ricorsivi di hasItem() e getItem() ricevono ora i valori per copia e trattano come assente un livello mancante; quello di unsetItem() scende solo nei livelli esistenti.
Core/HelperClasses/Session — rimossa la proprietà $sessionProperties
La proprietà privata $sessionProperties non veniva mai inizializzata né letta.
docs/configuration-reference.md — nuova costante
Documentata SESSION_TOKEN_INCLUDES_REMOTE_ADDRESS.
docs/security.md — token anti-dirottamento
La sezione sulla sessione precisa che il framework non chiama isValidSession() e quando escludere l'indirizzo IP dall'impronta.
File modificati:
Config/config.php, Core/HelperClasses/Config.php: aggiunta la costante SESSION_TOKEN_INCLUDES_REMOTE_ADDRESSCore/HelperClasses/Session.php: rimossa la proprietà $sessionProperties; getClientFingerprint() include l'indirizzo IP solo se SESSION_TOKEN_INCLUDES_REMOTE_ADDRESS è true; hasItemRecursive() e getItemRecursive() ricevono i valori per copia, unsetItemRecursive() scende solo nei livelli esistenti, e tutti e tre partono da $_SESSION con la chiave base come primo livelloTests/Core/HelperClasses/SessionTest.php: aggiunti testSessionTokenWithoutRemoteAddressSurvivesAddressChange(), testSessionTokenWithRemoteAddressIsInvalidatedByAddressChange() e testReadingMissingNestedItemDoesNotWriteSession(), che fallisce con il passaggio per riferimento; lo stub di Config restituisce la nuova costantedocs/configuration-reference.md: documentata la nuova costantedocs/security.md: aggiornata la descrizione del token anti-dirottamentoconfig.php del framework, e con il default il token è invariato.Session::hasItem(), getItem() e unsetItem() con chiavi annidate non creano più in sessione i livelli inesistenti. Il codice che, senza saperlo, contava su quei livelli vuoti (per esempio un hasItem('cart') vero dopo un hasItem('cart[items]')) li trova ora assenti.SESSION_TOKEN_INCLUDES_REMOTE_ADDRESS a false con sessioni aperte, isValidSession() restituisce false per le sessioni create prima della modifica.
Minor release che rivede la riscrittura della query string nel percorso eseguita dal dispatcher. Le richieste diverse da GET e HEAD non vengono più reindirizzate, i parametri di tracciamento restano nella query string, gli array annidati e i valori vuoti sopravvivono alla riscrittura e gli slash multipli in coda non causano più errori. Il rilascio rimuove inoltre gli header di cache pubblica applicati a tutte le risposte e introduce una cache configurabile per le sole risorse statiche.
Core/HelperClasses/Dispatcher — parametri preservati nella query string
Per le richieste GET e HEAD il dispatcher riscrive la query string nel percorso con un redirect (/blog?page=2 diventa /blog/page/2/). I parametri elencati nella nuova costante PRESERVED_QUERY_STRING_PARAMETERS restano invece nella query string del redirect (/blog?page=2&utm_source=x diventa /blog/page/2/?utm_source=x), e una richiesta che contiene solo questi parametri non viene reindirizzata. Prima la riscrittura li spostava nel percorso: gli strumenti di analytics e di tracciamento delle conversioni, che li leggono dalla query string lato client, non li ricevevano più, e lo stesso contenuto risultava raggiungibile da un URL diverso per ogni campagna. La lista predefinita comprende i parametri utm_* e gli identificativi di click di Google, Meta, Microsoft e Mailchimp. I parametri restano disponibili in $request->query e come argomenti delle action, con il nome convertito in camelCase (utmSource).
Config/config.php: nuova costante PRESERVED_QUERY_STRING_PARAMETERS, esposta come Config::$preservedQueryStringParametersCore/HelperClasses/Dispatcher/ResourceMaker — cache delle risorse statiche configurabile
Le risorse statiche servite dal framework (CSS, JavaScript, immagini, file scaricabili) ricevono ora Cache-Control: public, max-age=… ed Expires con la durata della nuova costante RESOURCE_CACHE_MAX_AGE, e l'header Pragma impostato da session_start() viene rimosso. L'header Expires usava inoltre il formato dei cookie (d-M-Y), non valido per questo header e trattato dai browser come una data già scaduta: ora usa il formato HTTP.
Config/config.php: nuova costante RESOURCE_CACHE_MAX_AGE (default 86400), esposta come Config::$resourceCacheMaxAgePublic/index.php — Cache-Control: public su tutte le risposte
index.php impostava Pragma: cache e Cache-control: public su ogni richiesta, dopo Session::start(), sostituendo gli header anti-cache inviati da session_start(). Un reverse proxy o una CDN potevano quindi mettere in cache e servire ad altri utenti pagine generate con i dati della sessione. Gli header sono stati rimossi: le pagine generate dai controller mantengono gli header anti-cache della sessione, e la cache pubblica è applicata solo alle risorse statiche.
Core/HelperClasses/Dispatcher::run() — corpo delle richieste POST perso con la query string
Il redirect della riscrittura veniva eseguito con qualunque metodo: una POST verso un URL con query string (un webhook come /api/webhook?provider=stripe o un form con action contenente parametri) veniva trasformata dal client in una GET e il corpo della richiesta andava perso. La riscrittura è ora eseguita solo per GET e HEAD; per gli altri metodi ActionArgumentsParser legge i parametri direttamente dalla query string e li unisce a quelli del percorso, che hanno la precedenza.
Core/Services/RouterService::reloadWithParsedQueryString() — errore fatale con parametri array
Un parametro array come ?tag[]=php&tag[]=orm veniva passato a urlencode(), che solleva un TypeError. Le liste semplici sono ora riscritte ripetendo la chiave (tag/php/tag/orm/), come già accettato dal parser; gli array associativi e annidati, come ?filter[status]=active&filter[tags][]=php, usano la notazione con parentesi codificata (filter%5Bstatus%5D/active/filter%5Btags%5D%5B%5D/php/), che ActionArgumentsParser ricostruisce con parse_str(). Prima le chiavi degli array associativi venivano perse.
Core/Services/RouterService::reloadWithParsedQueryString() — query string sulla root
Una richiesta alla root con query string, come /?page=2, veniva riscritta in /page/2/ e page veniva interpretato come nome del controller. La root viene ora riscritta nel percorso predefinito (/<DEFAULT_PATH>/<DEFAULT_ACTION>/page/2/).
Core/HelperClasses/Dispatcher/RouteResolver::parsePath() — valore vuoto in coda perso
Un parametro vuoto in fondo alla query string (?page=2&q=) produce un percorso che termina con q//. parsePath() rimuoveva con rtrim() tutti gli slash finali, q restava senza valore e veniva scartato. Viene ora rimosso un solo slash e q riceve il valore ''. injectDefaultController() è stato allineato: non usa più array_pop() per togliere l'action di default iniettata, che con un elemento vuoto in coda avrebbe rimosso quest'ultimo al posto dell'action, e gli argomenti passati ai CallableController sono ripuliti dagli elementi vuoti in coda come in precedenza.
Core/HelperClasses/Dispatcher/ActionArgumentsParser — TypeError con slash multipli in coda
Con più elementi vuoti in coda, come in /home/index///, cleanArrayShift() esauriva gli argomenti e restituiva null da un metodo con tipo di ritorno string, causando un errore 500. Il parser scarta ora gli elementi vuoti prima di ogni coppia chiave/valore e si ferma quando non ne resta una completa.
docs/configuration-reference.md — nuove costanti
Documentate RESOURCE_CACHE_MAX_AGE e PRESERVED_QUERY_STRING_PARAMETERS.
.htaccess — rimossa la regola su fbclid
La regola rimuoveva con un redirect 301 l'intera query string di ogni richiesta contenente fbclid, compresi gli altri parametri (?page=2&fbclid=… perdeva page), e impediva al tracciamento di Meta di ricevere l'identificativo di click. fbclid è ora gestito dalla lista dei parametri preservati. InstallationManager copia il .htaccess del framework nei nuovi progetti; nei progetti già installati la regola va rimossa a mano.
File modificati:
.htaccess: rimossa la regola di redirect su fbclidConfig/config.php, Core/HelperClasses/Config.php: aggiunte le costanti PRESERVED_QUERY_STRING_PARAMETERS e RESOURCE_CACHE_MAX_AGEPublic/index.php: rimossi gli header Pragma: cache e Cache-control: publicCore/HelperClasses/Dispatcher.php: nuovo parametro opzionale ?Config $customConfig nel costruttore; la condizione di riscrittura, estratta nel metodo privato hasQueryStringToParse(), è limitata a GET e HEAD ed esclude i parametri preservati; reloadWithParsedQueryString() riceve la Request del dispatcher; parseActionArguments() è chiamato anche per le action senza parametri; il nuovo metodo privato getCallableArguments() rimuove gli elementi vuoti in coda per i CallableControllerCore/HelperClasses/Dispatcher/ActionArgumentsParser.php: unione della query string con gli argomenti del percorso; cleanArrayShift() sostituito da removeLeadingEmptyArguments(); il nuovo metodo privato convertArgumentKey() converte in camelCase il solo nome base delle chiavi con parentesi, ricostruite con parse_str()Core/HelperClasses/Dispatcher/RouteResolver.php: parsePath() rimuove un solo slash finale; il nuovo metodo privato buildPathFromParts() conserva lo slash dell'elemento vuoto in coda; injectDefaultController() non inietta l'action di default invece di rimuoverla con array_pop()Core/HelperClasses/Dispatcher/ResourceMaker.php: il nuovo metodo privato sendCacheHeaders() imposta Cache-Control ed Expires e rimuove PragmaCore/Services/RouterService.php: reloadWithParsedQueryString() accetta un ?Config $customConfig opzionale, riscrive la root nel percorso predefinito, mantiene nella query string i parametri preservati e riscrive gli array con i nuovi metodi privati appendQueryArguments(), isScalarList() e appendQueryArgument()Tests/Core/HelperClasses/DispatcherTest.php: aggiunti testPostWithQueryStringDoesNotReload(), testPathWithTrailingEmptyParameter(), testGetWithOnlyPreservedQueryStringDoesNotReload(), testPathWithMultipleTrailingSlashes(), testPathWithEmptyValueFollowedByOtherParameters() e testPathWithNestedArrayParameters(); lo stub di Config restituisce i parametri preservatiTests/Core/Services/RouterServiceTest.php: aggiunti testReloadWithParsedQueryStringHandlesArrayValues(), testReloadWithParsedQueryStringHandlesNestedArrayValues(), testReloadWithParsedQueryStringOnRootPath() e testReloadWithParsedQueryStringExcludesPreservedParameters()Tests/Core/HelperClasses/RouterTest.php, Tests/Core/HelperClasses/ResourceMakerTest.php: lo stub di Config restituisce le nuove costantiTests/Console/Services/Installation/InstallationManagerTest.php: il .htaccess di prova è allineato a quello del frameworkdocs/configuration-reference.md: documentate le nuove costantiDispatcher::__construct() e RouterService::reloadWithParsedQueryString() sono opzionali, e le nuove costanti hanno un valore di default nel config.php del framework.POST, PUT, PATCH e DELETE con query string raggiungono il controller invece di essere reindirizzate; i parametri preservati restano nella query string invece di essere spostati nel percorso.$request->query contiene ora, con le chiavi in camelCase, anche gli argomenti delle action senza parametri e i parametri della query string delle richieste non reindirizzate. Il codice che lo legge con le chiavi originali (sort_order) in questi casi deve usare il nome in camelCase (sortOrder) o leggere $_GET, che resta invariato.Cache-Control: public e mantengono gli header anti-cache di session_start(); le risorse statiche sono invece memorizzabili in cache per RESOURCE_CACHE_MAX_AGE secondi.urlencode().
Minor release che rivede la cifratura delle proprietà delle entità. Ogni proprietà cifrata può ora avere un proprio vettore d'inizializzazione, rigenerato a ogni salvataggio, al posto di quello condiviso da tutte le proprietà cifrate della riga, che viene deprecato. Il rilascio corregge inoltre il riuso del vettore condiviso negli update, la perdita dei valori cifrati '' e '0' in lettura e adotta random_bytes() per la generazione dei vettori.
Orm/BaseClasses/BaseEntity — vettore d'inizializzazione per proprietà
Una proprietà cifrata usa come vettore d'inizializzazione la proprietà con lo stesso nome seguito dal suffisso InitializationVector (per esempio privateKeyInitializationVector, colonna private_key_initialization_vector), se l'entità la dichiara. A ogni salvataggio della proprietà l'ORM genera un nuovo vettore e lo scrive insieme al valore cifrato: proprietà diverse e versioni successive della stessa proprietà non condividono mai il vettore, e un'entità caricata solo in parte può essere salvata senza rendere illeggibili le proprietà cifrate non caricate.
In lettura il valore viene decifrato con il vettore proprio o, se questo è NULL o assente, con quello condiviso: le righe scritte prima dell'adozione del vettore proprio restano leggibili e vengono ricifrate al primo salvataggio.
Config/config.php: nuova costante ENCRYPTED_PROPERTY_INITIALIZATION_VECTOR_SUFFIX (default "InitializationVector"), esposta come Config::$encryptedPropertyInitializationVectorSuffixBaseEntity::getPropertyInitializationVectorName(): nuovo metodo pubblico, restituisce il nome della proprietà che contiene il vettore di una proprietà cifrata, il vettore proprio se dichiarato e altrimenti quello condivisoBaseEntity::isInitializationVector(): riconosce anche i vettori propri delle proprietà cifrate, che il DataMapper esclude quindi dai valori ordinari come già faceva per il vettore condivisoOrm/HelperClasses/DataMapper::parseSingleProperty() — vettore d'inizializzazione riusato negli update
Il vettore condiviso veniva generato solo se l'entità non ne aveva già uno: a ogni update le proprietà cifrate venivano ricifrate con lo stesso vettore e la stessa chiave. Con AES-256-CBC questo rivela quando due versioni di un campo iniziano con gli stessi 16 byte; con modalità a flusso come CTR esporrebbe il contenuto. Il vettore viene ora rigenerato a ogni salvataggio. Fa eccezione il vettore condiviso di un'entità caricata solo in parte: se una delle proprietà cifrate che lo usano non è inizializzata, non verrebbe ricifrata e diventerebbe illeggibile, quindi il vettore caricato viene mantenuto, come nelle versioni precedenti. Con il vettore condiviso il rischio resta inoltre tra proprietà diverse della stessa riga, che lo condividono per costruzione: per eliminarlo va adottato il vettore per proprietà, che non ha eccezioni perché ogni vettore viene scritto insieme alla sua proprietà. L'eccezione sarà rimossa nella 13.0.0 insieme al vettore condiviso.
Core/HelperClasses/Encryptor::createInitializationVector() — random_bytes() al posto di openssl_random_pseudo_bytes()
I vettori d'inizializzazione sono ora generati con random_bytes(), il generatore crittograficamente sicuro standard di PHP, che non dipende dall'estensione OpenSSL e solleva un'eccezione se non può produrre byte sicuri. openssl_random_pseudo_bytes() era già sicura dalla PHP 7.4 in poi: si tratta di un rafforzamento, non della correzione di vettori deboli.
Orm/BaseClasses/BaseResultSet::convertToBaseEntity() — valori cifrati '' e '0' letti come testo cifrato
Il risultato di Encryptor::decryptString() era scelto con l'operatore ?:, che scarta tutti i valori falsy e non solo il false restituito in caso di errore: una proprietà cifrata con valore '' o '0' veniva idratata con il testo cifrato. Il valore decifrato viene ora scartato solo se è false.
File modificati:
Config/config.php, Core/HelperClasses/Config.php: aggiunta la costante ENCRYPTED_PROPERTY_INITIALIZATION_VECTOR_SUFFIXCore/HelperClasses/Encryptor.php: createInitializationVector() usa random_bytes()Orm/BaseClasses/BaseEntity.php: aggiunto getPropertyInitializationVectorName(); isEncryptedProperty() verifica il vettore della singola proprietà; isInitializationVector() riconosce i vettori propriOrm/BaseClasses/BaseResultSet.php: la decifratura, estratta nel nuovo metodo privato decryptValue(), usa il vettore proprio con ripiego su quello condiviso e scarta il risultato solo se è falseOrm/HelperClasses/DataMapper.php: parseSingleProperty() usa il vettore della singola proprietà e lo rigenera a ogni salvataggio; il nuovo metodo privato initializationVectorIsRenewable() mantiene il vettore caricato quando una delle proprietà cifrate che lo usano non è inizializzataTestsApplication/Entities/EntityWithOwnInitializationVectors.php: nuova entità con due proprietà cifrate, i rispettivi vettori e il vettore condivisoTests/Orm/BaseClasses/BaseEntityTest.php: aggiunto testPropertyInitializationVectorName()Tests/Orm/HelperClasses/DataMapperTest.php: aggiunti testInsertEncryptsEachPropertyWithItsOwnInitializationVector() e testUpdateRewritesOnlyInitializationVectorOfSavedProperty(), che falliscono con il vettore condiviso, e testUpdatePartiallyLoadedEntityKeepsSharedInitializationVector(), che fallisce se il vettore condiviso viene rigenerato anche con una proprietà cifrata non caricata; testSaveNewEntityWithEncryptedPropertyOne(), testSaveNewEntityWithEncryptedPropertyTwo() e testSaveEntityWithNullEncryptedPropertyKeepsNull() verificano che l'update scriva un vettore nuovoTests/Orm/ResultSets/ResultSetMysqlTest.php: aggiunti testFetchWithFalsyEncryptedPropertyValues(), testFetchWithOwnInitializationVectors() e testFetchWithOwnInitializationVectorFallsBackToSharedInitializationVector()Tests/Orm/BaseClasses/BaseModelTest.php, Tests/Orm/ExtendedClasses/DependentModelTest.php, Tests/Orm/ExtendedClasses/SelfDependentModelTest.php, Tests/Orm/JoinEagerLoadingTest.php: lo stub di Config restituisce il suffisso del vettoredocs/orm.md: sezione "Proprietà Cifrate" riscritta con il vettore per proprietà, il vettore condiviso deprecato, la procedura di migrazione e le query sulle colonne cifratedocs/configuration-reference.md: documentata ENCRYPTED_PROPERTY_INITIALIZATION_VECTOR_SUFFIXdocs-phpdoc/: rigenerate le pagine di BaseEntity, BaseResultSet, ReferencedEntity, SelfReferencedEntity e DataMapper e l'indice di ricercainitializationVector): deprecato dalla versione 12.11.0, sarà rimosso nella versione 13.0.0. Dichiarare per ogni proprietà cifrata la proprietà <proprietà>InitializationVector e la relativa colonna, seguendo la procedura di migrazione in docs/orm.md. Con il vettore condiviso sarà rimossa anche l'eccezione che lo mantiene sulle entità caricate solo in parte. Non viene emesso alcun avviso a runtime.initializationVector continuano a funzionare con il vettore condiviso, e i dati esistenti restano leggibili senza migrazione.'' o '0' vengono ora restituite decifrate.Patch che completa la 12.10.0 e la 12.10.1, estendendo il controllo sui tipi nativi e la cache dei metadati di reflection ai punti dell'ORM che non eseguono il controllo per ogni riga, ma per ogni classe di entità o per ogni query.
Orm/BaseClasses/BaseEntity, Orm/BaseClasses/BaseAdapter, Orm/ExtendedClasses/DependentModel, Orm/HelperClasses/Cache — autoload dei nomi dei tipi nativi
Come nei punti corretti dalla 12.10.1, questi controlli passavano il nome del tipo di ogni proprietà a is_subclass_of(), e per le proprietà di tipo nativo l'autoloader veniva invocato con nomi come int o string, eseguendo un file_exists() che fallisce sempre. I controlli verificano ora che il tipo non sia nativo prima di confrontarlo con una classe:
BaseEntity::getForeignKeyPropertyNames(), alla prima istanziazione di ogni classe di entità;BaseEntity::forceForeignKeyPropertySet(), per coerenza con gli altri controlli: il ramo è raggiunto solo per le proprietà con un indice di foreign key in attesa di risoluzione, che sono sempre entità;BaseAdapter::buildJoinedColumns(), per ogni colonna delle entità in join;DependentModel::buildPropertiesConditions(), per ogni proprietà usata come condizione nelle query per proprietà;Cache, durante la costruzione della cache delle foreign key, per ogni proprietà di ogni entità dei moduli.Un tipo nativo non può essere un'entità, quindi il risultato dei controlli non cambia.
Orm/HelperClasses/Cache — reflection delle entità ricreata
La costruzione della cache delle foreign key istanziava una \ReflectionClass per ogni entità dei moduli. Usa ora BaseEntity::getFinalClassReflectionProperties(), introdotto nella 12.10.0, filtrando le proprietà protected come prima: le proprietà lette restano le stesse e i metadati sono condivisi con il resto dell'ORM.
File modificati:
Orm/BaseClasses/BaseEntity.php: controllo isBuiltin() in getForeignKeyPropertyNames() e forceForeignKeyPropertySet()Orm/BaseClasses/BaseAdapter.php: controllo isBuiltin() in buildJoinedColumns()Orm/ExtendedClasses/DependentModel.php: controllo isBuiltin() in buildPropertiesConditions()Orm/HelperClasses/Cache.php: proprietà lette da BaseEntity::getFinalClassReflectionProperties() e controllo isBuiltin() nella costruzione della cache delle foreign keyTests/Orm/BaseClasses/BaseEntityTest.php: in testBuiltinPropertiesDoNotAutoloadTypeNames() l'entità è istanziata dopo la registrazione dell'autoloader di controllo, così che il test copra anche la ricerca delle foreign key nel costruttore. Fallisce senza il controllo in getForeignKeyPropertyNames(), se la classe non è già stata istanziata da un test precedentedocs-phpdoc/: rigenerata la pagina di Cache (numeri di riga)
Patch che elimina il principale costo a runtime dell'ORM: per ogni proprietà di tipo nativo (int, string, float, bool) l'idratazione, l'assegnazione, toArray() e il salvataggio di un'entità invocavano l'autoloader con il nome del tipo, e ogni invocazione cercava su disco un file inesistente.
Orm/BaseClasses/BaseEntity e Orm/HelperClasses/DataMapper — autoload dei nomi dei tipi nativi
Per capire se una proprietà fosse una foreign key o un tipo custom, l'ORM passava il nome del suo tipo a is_subclass_of() e is_a(..., true). Quando ricevono una stringa, le due funzioni caricano la classe se non è già definita: per i tipi nativi l'autoloader veniva quindi invocato con nomi come int o string. Con l'autoloader del framework ogni invocazione crea un Autoloader ed esegue un file_exists() che fallisce sempre, un esito che PHP non conserva nella sua cache. Per un'entità con 10 colonne scalari, idratata e poi letta, modificata e convertita con toArray(), l'autoloader veniva invocato circa 23 volte per riga.
I controlli verificano ora che il tipo non sia nativo prima di confrontarlo con una classe:
switchSettingType(), eseguito a ogni assegnazione e quindi per ogni colonna idratata;isCustomType(), eseguito dal tracciamento delle modifiche a ogni assegnazione;parsePropterty(), eseguito per ogni proprietà da toArray();DataMapper::parseSingleProperty(), eseguito per ogni proprietà salvata.Un tipo nativo non può essere né un'entità né un tipo custom, quindi il risultato dei controlli non cambia.
Misurato con l'autoloader del framework su un'entità con 10 colonne scalari (idratazione, due letture, un'assegnazione e toArray() per riga), il tempo per riga passa da ~570–710 µs a ~37 µs. Il guadagno effettivo dipende dal costo di file_exists() sul filesystem in uso ed è massimo su Windows e su filesystem sincronizzati o di rete.
File modificati:
Orm/BaseClasses/BaseEntity.php: controllo isBuiltin() in switchSettingType(), isCustomType() e parsePropterty()Orm/HelperClasses/DataMapper.php: controllo isBuiltin() in parseSingleProperty()Tests/Orm/BaseClasses/BaseEntityTest.php: aggiunto testBuiltinPropertiesDoNotAutoloadTypeNames(), che registra un autoloader e verifica che assegnazioni e toArray() su proprietà native non richiedano classi senza namespace. Fallisce contro l'implementazione precedenteTests/Orm/HelperClasses/DataMapperTest.php: aggiunto testSaveDoesNotAutoloadBuiltinTypeNames(), la stessa verifica sul salvataggio. Fallisce contro l'implementazione precedentedocs-phpdoc/: rigenerate le pagine di BaseEntity, ReferencedEntity e SelfReferencedEntity (numeri di riga)
Minor release che riduce il costo a runtime dell'ORM: gli oggetti \ReflectionClass e \ReflectionProperty delle entità, finora ricreati a ogni istanziazione, lettura, scrittura, salvataggio e idratazione, sono ora costruiti una sola volta per classe e riutilizzati per tutta la durata della richiesta. La cache è esposta da tre nuovi metodi statici pubblici di BaseEntity, usati da tutte le componenti dell'ORM che leggevano la struttura delle entità.
Orm/BaseClasses/BaseEntity — getReflectionClass(), getReflectionProperty() e getFinalClassReflectionProperties()
I tre metodi accettano il nome di una classe di entità o un'istanza (string|BaseEntity) e restituiscono metadati memorizzati in cache statiche indicizzate per classe:
getReflectionClass() restituisce la \ReflectionClass dell'entità.getReflectionProperty() restituisce la \ReflectionProperty della proprietà indicata.getFinalClassReflectionProperties() restituisce, indicizzate per nome, le proprietà dichiarate in classi non astratte (quelle che checkFinalClassReflectionProperty() riconosce come colonne dell'entità), nell'ordine di dichiarazione.Chiamate successive per la stessa classe restituiscono le stesse istanze, in qualunque ordine i tre metodi vengano invocati.
Orm/BaseClasses/BaseEntity — reflection ricreata a ogni istanziazione e a ogni accesso alle proprietà
Il costruttore istanziava una \ReflectionClass e scorreva tutte le proprietà per individuare le foreign key; __get() e __set() (tramite checkFinalClassProperty(), forceForeignKeyPropertySet() e switchSettingType()) creavano una nuova \ReflectionProperty a ogni accesso; toArray() ripeteva la scansione completa delle proprietà. L'elenco delle foreign key è ora calcolato una sola volta per classe e tutti gli accessi usano i metadati in cache. forceForeignKeyPropertySet() legge inoltre la reflection solo quando la proprietà non è valorizzata e ha un indice di foreign key in attesa di risoluzione, invece che a ogni lettura.
Orm/BaseClasses/BaseResultSet, Orm/HelperClasses/DataMapper, Orm/BaseClasses/BaseModel, Orm/BaseClasses/BaseAdapter, Orm/ExtendedClasses/DependentModel — reflection ricreata per ogni riga e per ogni proprietà
L'idratazione dei risultati creava una \ReflectionProperty per ogni colonna di ogni riga, il salvataggio scansionava tutte le proprietà dell'entità a ogni save(), e le query per proprietà e per relazione ricreavano la reflection a ogni chiamata. Tutti questi punti usano ora i metodi di BaseEntity; nell'idratazione il nome della colonna del vettore di inizializzazione è calcolato una volta per riga invece che per ogni colonna.
Orm/BaseClasses/BaseResultSet::convertToBaseEntity() — colonna del vettore di inizializzazione assente dal risultato
Per una proprietà cifrata con valore non nullo, l'entità principale leggeva la colonna del vettore di inizializzazione senza verificarne la presenza: se la query non la selezionava, l'accesso sollevava il warning Undefined property su \stdClass. Il controllo isset() già presente nell'idratazione delle entità collegate è ora applicato anche all'entità principale; in assenza del vettore il valore è restituito così com'è.
Orm/BaseClasses/BaseResultSet::hydrateRelatedEntity() e Orm/ExtendedClasses/ReferencedEntity::toArray() — rimozione della logica duplicata
hydrateRelatedEntity() duplicava convertToBaseEntity() ed è stato rimosso: l'idratazione delle entità collegate usa ora convertToBaseEntity(), che accetta la classe dell'entità come secondo parametro opzionale (?string $entityClass = null, di default il tipo di ritorno del result set). ReferencedEntity::toArray() duplicava BaseEntity::toArray() e ora vi aggiunge soltanto le collezioni.
File modificati:
Orm/BaseClasses/BaseEntity.php: aggiunte le cache statiche e i metodi getReflectionClass(), getReflectionProperty() e getFinalClassReflectionProperties(); foreign key calcolate una volta per classe; checkFinalClassProperty(), forceForeignKeyPropertySet(), switchSettingType() e toArray() usano i metadati in cacheOrm/BaseClasses/BaseResultSet.php: convertToBaseEntity() accetta la classe dell'entità, usa la reflection in cache e verifica la presenza della colonna del vettore di inizializzazione; rimosso hydrateRelatedEntity()Orm/BaseClasses/BaseModel.php: risoluzione dei percorsi di relazione, appendRelationJoin() e costruzione delle condizioni per proprietà con la reflection in cacheOrm/BaseClasses/BaseAdapter.php: buildJoinedColumns() scorre le proprietà in cacheOrm/ExtendedClasses/DependentModel.php: buildPropertiesConditions() usa la reflection in cacheOrm/ExtendedClasses/ReferencedEntity.php: toArray() delega a BaseEntity::toArray()Orm/HelperClasses/DataMapper.php: parseValues() scorre le proprietà in cache, già filtrate; rimosso il filtro corrispondente da propertyIsParsable()Tests/Orm/BaseClasses/BaseEntityTest.php: aggiunti testReflectionMetadataIsCachedPerEntityClass() e testReflectionPropertyCachedBeforeFinalClassPropertiesIsReused(), che verifica che la \ReflectionProperty ottenuta prima della scansione delle proprietà non venga sostituita da un'altra istanza; il setUp() registra un BaseAdapter di default, senza il quale la classe di test falliva se eseguita da solaTests/Orm/ResultSets/ResultSetMysqlTest.php: il setUp() registra un BaseAdapter di default, per lo stesso motivoTestsApplication/Entities/ReflectionCacheSample.php: nuova entità usata solo dal test sull'ordine di accesso alla cache, perché le cache statiche non sono già popolate da altri testdocs/performance.md: aggiunta la sezione "Cache dei Metadati di Reflection"docs/join-eager-loading.md: i riferimenti a hydrateRelatedEntity() sostituiti con convertToBaseEntity()docs-phpdoc/: rigenerate le pagine di BaseEntity, BaseResultSet, ReferencedEntity, SelfReferencedEntity e DataMapper e l'indice di ricercaBaseEntity sono aggiunte, e il nuovo parametro di convertToBaseEntity(), metodo privato, è opzionale.
Patch che corregge DependentModel::getOtherEntityCollectionByEntity() e SelfDependentModel::getOtherEntityCollectionByParent(), i metodi che restituiscono le entità collegate alla stessa entità (o allo stesso padre) di un'entità data, esclusa quest'ultima: la query generata faceva riferimento a una colonna inesistente, e il primo dei due metodi perdeva anche il valore dell'entità esclusa e non accettava null.
Orm/ExtendedClasses/DependentModel::getOtherEntityCollectionByEntity() e Orm/ExtendedClasses/SelfDependentModel::getOtherEntityCollectionByParent() — condizione di esclusione sulla colonna id_id
La condizione che esclude l'entità passata veniva aggiunta con appendCondition('id', ComparisonOperator::notEqual, Placeholder::placeholder, true). Il quarto parametro indica una foreign key, il cui nome colonna riceve il suffisso Id (NotationManager::convertPropertyNameToColumnName()): la condizione diventava id_id <> ?, su una colonna che nella tabella non esiste, e la query falliva con un errore SQL. La condizione è ora generata sulla colonna id, come in BaseModel::getOtherEntityCollection().
Orm/ExtendedClasses/DependentModel::getOtherEntityCollectionByEntity() — valore dell'entità esclusa perso
Quando $baseEntity era valorizzata, il suo valore e il suo tipo sostituivano gli array $bindValues e $bindTypes invece di esservi aggiunti: la query aveva due placeholder (id <> ? AND ..._id = ?) e un solo valore, con un errore sul numero dei parametri o, a seconda del driver, l'entità di riferimento legata al placeholder dell'esclusione. I due valori sono ora entrambi legati, nell'ordine dei placeholder.
Orm/ExtendedClasses/DependentModel::getOtherEntityCollectionByEntity() — null non accettato
Il metodo prevede un ramo per $baseEntity === null, che genera la condizione ..._id IS NULL, ma il parametro era dichiarato BaseEntity: passare null sollevava un TypeError prima di eseguire il metodo, e il ramo non era raggiungibile. Il parametro è ora ?BaseEntity $baseEntity = null, come $parentEntity in SelfDependentModel::getOtherEntityCollectionByParent().
I test esistenti verificavano solo il numero delle chiamate ad appendCondition(), senza controllarne gli argomenti né i valori passati a DataMapper::find(). Inoltre testGetOtherEntityCollectionByEntityWithNullEntity(), nonostante il nome, passava un'entità invece di null.
File modificati:
Orm/ExtendedClasses/DependentModel.php: getOtherEntityCollectionByEntity() genera la condizione di esclusione sulla colonna id, aggiunge valore e tipo di $baseEntity ai bind invece di sostituirli e accetta ?BaseEntity $baseEntity = nullOrm/ExtendedClasses/SelfDependentModel.php: getOtherEntityCollectionByParent() genera la condizione di esclusione sulla colonna idTests/Orm/ExtendedClasses/DependentModelTest.php: testGetOtherEntityCollectionByEntity() e testGetOtherEntityCollectionByEntityWithNullEntity() verificano gli argomenti di ogni chiamata ad appendCondition() e i valori e i tipi passati a find(); il secondo passa ora null. Entrambi falliscono contro l'implementazione precedenteTests/Orm/ExtendedClasses/SelfDependentModelTest.php: testGetOtherEntityCollectionByParent() verifica gli argomenti di appendCondition() e di find(); aggiunto testGetOtherEntityCollectionByParentWithNullParent(). Entrambi falliscono contro l'implementazione precedentedocs/orm.md: documentati getOtherEntityCollectionByEntity() nella sezione "Metodi per Relazioni" e getOtherEntityCollectionByParent() nella sezione "Query Gerarchiche"docs-phpdoc/: rigenerate le pagine di DependentModel e SelfDependentModel, che la eredita (firma di getOtherEntityCollectionByEntity())getOtherEntityCollectionByEntity() passa da BaseEntity $baseEntity a ?BaseEntity $baseEntity = null, un allargamento compatibile con tutte le chiamate esistenti.BaseEntity $baseEntity restringe il tipo del parametro e PHP lo rifiuta con un errore fatale al caricamento della classe; va allineato a ?BaseEntity $baseEntity = null.
Minor release che risolve alla radice il problema delle costanti di configurazione introdotte dopo l'installazione di un progetto: Config usa ora come valori di default quelli del config.php del modulo SismaFramework, quindi un progetto che aggiorna il framework non deve più aggiungere le nuove costanti al proprio configFramework.php. Su questa base, i parametri di Session e della cifratura con password di Encryptor, che la 12.8.x aveva introdotto come costanti di classe, sono spostati in Config/config.php, come tutti gli altri parametri del framework; l'involucro della cifratura con password passa al formato sisma-pw2, che dichiara tutti i propri parametri.
Core/HelperClasses/Config — valori di default dal config.php del modulo
Config/configFramework.php viene copiato da Config/config.php una sola volta, in fase di installazione: le costanti aggiunte al framework dopo l'installazione mancavano nel progetto, e la prima lettura sollevava Error: Undefined constant (vedi la 12.3.0, dove l'aggiunta manuale era documentata come azione richiesta).
Il config.php del modulo dichiara ora le costanti nel namespace SismaFramework\Config, che corrisponde alla sua posizione nel modulo, e l'installazione lo copia in configFramework.php riscrivendo il namespace in Config. Entrambi i file possono quindi essere caricati insieme: Config cerca ogni costante prima in Config\ (il progetto) e, se non la trova, carica il config.php del modulo e la legge da SismaFramework\Config\. Se la costante manca anche lì, l'errore resta Error: Undefined constant. Il file del modulo viene caricato solo quando manca una costante: un progetto con la configurazione completa non lo legge mai.
Il default è il valore calcolato nel file del modulo. Per le costanti scalari (durate, iterazioni, algoritmi) è il valore corretto; una costante derivata da altre costanti o da __DIR__ (path, namespace) avrebbe invece il valore calcolato sulla cartella del modulo e sull'applicazione Sample, e va dichiarata nel progetto.
Core/HelperClasses/Session — durata e rotazione configurabiliLa durata e gli attributi del cookie di sessione, il token anti-dirottamento e i parametri della rotazione dell'ID introdotti nella 12.8.2 erano scritti nella classe: per cambiarli bisognava modificare il framework, e l'aggiornamento successivo li riportava al valore originale. Sono ora costanti di configurazione:
SESSION_LIFETIME (3600): durata del cookie di sessione, a cui start() allinea session.gc_maxlifetimeSESSION_COOKIE_PATH (/), SESSION_COOKIE_HTTP_ONLY (true), SESSION_COOKIE_SAME_SITE (Lax): attributi del cookie di sessione, prima modificabili solo con un override di SessionSESSION_COOKIE_DOMAIN (""): attributo Domain del cookie. Vuoto, come dalla 12.8.2, il cookie resta legato all'host esatto; impostato su un dominio, la sessione è condivisa con i sottodomini, cosa che la 12.8.2 permetteva solo con un override di SessionSESSION_TOKEN_KEY (token), SESSION_TOKEN_HASH_ALGORITHM (sha512): chiave e algoritmo del token anti-dirottamento, prima ripetuti in start() e isValidSession()SESSION_ROTATION_GRACE_SECONDS (30): periodo in cui un ID già ruotato resta agganciato alla sessione che lo ha sostituitoSESSION_MAXIMUM_ROTATION_HOPS (10): numero massimo di rotazioni consecutive seguiteSESSION_ROTATED_AT_KEY (__sismaSessionRotatedAt), SESSION_NEXT_SESSION_ID_KEY (__sismaNextSessionId): chiavi con cui la sessione ruotata registra l'istante della rotazione e il nuovo ID, prima costanti pubbliche di SessionCore/HelperClasses/Encryptor — cifratura con password configurabile e involucro sisma-pw2I parametri della cifratura con password introdotta nella 12.8.0 erano costanti di classe. Sono ora costanti di configurazione:
PASSWORD_KEY_DERIVATION_DEFAULT_ITERATIONS (600000), PASSWORD_KEY_DERIVATION_MINIMUM_ITERATIONS (100000), PASSWORD_KEY_DERIVATION_MAXIMUM_ITERATIONS (10000000): costo di default e intervallo ammesso di PBKDF2PASSWORD_KEY_DERIVATION_HASH_ALGORITHM (sha256), PASSWORD_KEY_DERIVATION_SALT_BYTES (16): hash di PBKDF2 e lunghezza del salePASSWORD_ENCRYPTION_ALGORITHM (aes-256-gcm), PASSWORD_ENCRYPTION_INITIALIZATION_VECTOR_BYTES (12), PASSWORD_ENCRYPTION_TAG_BYTES (16): cifrario e lunghezze di IV e tagPASSWORD_ENCRYPTION_MINIMUM_TAG_BYTES (12): lunghezza minima del tag accettataPASSWORD_ENVELOPE_VERSION (sisma-pw2), PASSWORD_ENVELOPE_SEPARATOR ($), PASSWORD_ENVELOPE_KEY_DERIVATION_PREFIX (pbkdf2-): definizione del formato dell'involucro. A differenza degli altri parametri, valgono anche in decifratura: cambiarli rende illeggibili gli involucri creati con i valori precedenti finché non vengono ripristinati
L'involucro sisma-pw1 dichiarava iterazioni e sale ma non il cifrario, e la decifratura confrontava le lunghezze di IV e tag con valori fissi: con questi parametri configurabili, cambiarne uno avrebbe reso indecifrabili gli involucri esistenti. I nuovi involucri usano quindi il formato sisma-pw2$<cifrario>$pbkdf2-<hash>$<iterazioni>$<sale>$<iv>$<tag>$<dati>: decryptWithPassword() legge cifrario, hash e iterazioni dall'intestazione, IV e tag dai dati, e ricava la lunghezza della chiave dal cifrario. La configurazione decide solo come vengono creati i nuovi involucri, e modificarla non rende indecifrabili quelli esistenti; dalla configurazione vengono letti in decifratura solo i limiti (iterazioni minime e massime, tag minimo), così un involucro alterato non può imporre una derivazione troppo debole o troppo onerosa né indebolire la verifica di integrità troncando il tag. L'intestazione, cifrario compreso, resta autenticata insieme ai dati. Gli involucri sisma-pw1 non sono più supportati: il formato è stato sostituito il giorno dopo l'uscita della 12.8.0, e continuare a leggerlo avrebbe richiesto di conservare nel codice i suoi parametri fissi.
encryptWithPassword() solleva InvalidArgumentException anche se la configurazione indica un hash non supportato da hash_hmac_algos(), un cifrario che non è un cifrario autenticato supportato da OpenSSL (aes-128/192/256-gcm, -ccm, -ocb, chacha20-poly1305) o un tag più corto del minimo.
Anche la lunghezza dei token di getSimpleRandomToken(), prima fissa a 16 byte, è ora la costante SIMPLE_RANDOM_TOKEN_BYTES (16); il metodo accetta ?Config $customConfig = null come gli altri metodi di Encryptor.
Security/HttpClasses/Authentication — token CSRF e campi del form configurabili
I nomi del token CSRF e dei campi da cui Authentication legge le credenziali erano scritti nella classe. Sono ora costanti di configurazione:
CSRF_TOKEN_KEY (csrfToken): chiave di sessione e campo del form del token CSRF, verificati da checkCsrfToken()AUTHENTICATION_IDENTIFIER_FIELD (identifier), AUTHENTICATION_PASSWORD_FIELD (password), AUTHENTICATION_MULTI_FACTOR_CODE_FIELD (code): campi letti da checkAuthenticable(), checkPassword() e checkMultiFactor()
Il costruttore accetta un ?Config $customConfig = null opzionale, come le altre classi che leggono la configurazione.
Security/HttpClasses/OAuthAuthentication — chiave di sessione dello state configurabile
La chiave di sessione in cui getAuthorizationUrl() salva lo state anti-CSRF, e da cui checkCallback() lo rilegge, era oauthState, scritta nella classe. È ora la costante di configurazione OAUTH_STATE_KEY (oauthState), e la lunghezza dello state, prima fissa a 16 byte, è OAUTH_STATE_BYTES (16); il costruttore accetta ?Config $customConfig = null come Authentication. I parametri error, code e state della richiesta di callback restano invariati: sono definiti dal protocollo OAuth 2.0.
Security/HttpClasses/Authentication::checkCsrfToken() — confronto del token non a tempo costante
Il token inviato dal form era confrontato con quello in sessione tramite ===, il cui tempo di esecuzione dipende dalla posizione del primo carattere diverso. Il confronto usa ora hash_equals(), come già checkOAuthState() di OAuthAuthentication e Session::isValidSession(). La documentazione di OAuthAuthentication affermava già che i due controlli seguivano lo stesso schema: ora è vero.
Core/HelperClasses/Session — rotazione dell'ID con nomi espressivi
followRotation() e rotate(), introdotti nella 12.8.2 con due docblock esplicativi, sono sostituiti da followRotationChainOfPresentedId() e rotateIdKeepingPreviousSessionDuringGracePeriod(), scomposti in isCurrentSessionRotated(), isRotationStillFollowable(), replaceWithEmptySession(), markCurrentSessionAsRotatedTo() e switchToSession(). I docblock sono rimossi. Il comportamento non cambia.
docs/configuration-reference.md — esempi di configurazione erratirequire_once 'config.defaults.php', un file che non esiste: la riga è rimossa, e l'esempio minimale spiega quali costanti possono essere omesse grazie ai valori di default del modulo.ENCRYPTION_ALGORITHM = 'AES-256-GCM': encryptString() non passa il tag di autenticazione a openssl_encrypt(), che con GCM emette un warning e restituisce false, quindi la cifratura delle proprietà falliva. Il suggerimento è rimosso e sostituito da una nota.docs/security.md — algoritmi deboli suggeriti per SIMPLE_HASH_ALGORITHM
L'esempio di configurazione proponeva md5 e sha1 come alternative a sha256. Sono sostituiti da sha384, sha512 e sha3-256, con una nota che sconsiglia gli algoritmi vulnerabili alle collisioni e ricorda di usare getBlowfishHash() per le password.
docs/security.md — esempio del token CSRF salvato nella chiave sbagliata
L'esempio di getSimpleRandomToken() salvava il token CSRF in $_SESSION['csrf_token'], mentre checkCsrfToken() lo cerca in csrfToken: un'applicazione che seguiva l'esempio non superava mai il controllo. L'esempio usa ora Session::setItem('csrfToken', ...). Corretto anche il token d'esempio, lungo 40 caratteri invece di 32.
File modificati:
Config/config.php: namespace SismaFramework\Config; aggiunte le costanti SESSION_*, PASSWORD_*, SIMPLE_RANDOM_TOKEN_BYTES, CSRF_TOKEN_KEY, AUTHENTICATION_* e OAUTH_STATE_* elencate sopraConfig/openssl.cnf: nuovo file con la configurazione OpenSSL minimale usata quando OPENSSL_CONFIG_PATH non è valorizzata, prima contenuta nella costante Encryptor::MINIMAL_OPENSSL_CONFIGCore/HelperClasses/Config.php: aggiunto getProjectOrFrameworkValue(), che legge la costante del progetto o, in sua assenza, quella del modulo; aggiunte le proprietà delle nuove costantiCore/HelperClasses/Session.php: durata e attributi del cookie, token e parametri della rotazione letti da Config; il calcolo del token, prima ripetuto in start() e isValidSession(), è estratto in getClientFingerprint(); rimosse le costanti pubbliche ROTATION_GRACE_SECONDS, ROTATED_AT_KEY e NEXT_SESSION_ID_KEY e la costante privata MAXIMUM_ROTATION_HOPS; rotazione scomposta come descritto sopraCore/HelperClasses/Encryptor.php: involucro sisma-pw2 con parametri letti da Config, senza supporto ai sisma-pw1; configurazione OpenSSL di ripiego letta da Config/openssl.cnf invece che scritta in un file temporaneo; rimosse le costanti pubbliche PASSWORD_KEY_DERIVATION_DEFAULT_ITERATIONS e PASSWORD_KEY_DERIVATION_MINIMUM_ITERATIONS, tutte le costanti private (parametri e formato dell'involucro, MINIMAL_OPENSSL_CONFIG) e il metodo getMinimalOpensslConfigPath(); encryptWithPassword() accetta ?int $iterations = null e ?Config $customConfig = null, decryptWithPassword() e getSimpleRandomToken() accettano ?Config $customConfig = null; getSimpleRandomToken() legge la lunghezza del token da ConfigSecurity/HttpClasses/Authentication.php: checkCsrfToken(), checkAuthenticable(), checkPassword() e checkMultiFactor() leggono i nomi del token e dei campi da Config; switchMultiFactorOptions() riceve il codice come argomento invece di rileggerlo dalla richiesta; checkCsrfToken() confronta i token con hash_equals(); il costruttore accetta ?Config $customConfig = nullSecurity/HttpClasses/OAuthAuthentication.php: getAuthorizationUrl() e checkCallback() leggono chiave di sessione e lunghezza dello state da Config; il costruttore accetta ?Config $customConfig = nullConsole/Services/Installation/InstallationManager.php: copyConfigFolder() riscrive il namespace del file copiato in ConfigTestsApplication/HelperClasses/ConfigWithUndeclaredParameter.php: nuova fixture con un parametro senza costante né nel progetto né nel moduloTestsApplication/Entities/EntityWithNullableEncryptedProperty.php: rimossa la descrizione dal docblock di classeTests/Core/HelperClasses/ConfigTest.php: aggiunti testProjectConstantTakesPrecedenceOverFrameworkConfiguration(), testUndeclaredProjectConstantFallsBackToFrameworkConfiguration() (entrambi in un processo separato) e testConstantDeclaredNowhereThrowsError()Tests/Core/HelperClasses/EncryptorTest.php: lo stub di Config include i nuovi parametri; i test esistenti adeguati al formato sisma-pw2; aggiunti testDecryptWithPasswordRejectsReleasedVersion1Envelope() (involucro prodotto dalla 12.8.2), testEnvelopeStaysDecryptableAfterEncryptionParametersChange(), testEnvelopeFormatIsReadOnlyWithSameFormatConfiguration(), testEncryptWithPasswordRejectsCipherWithoutAuthentication(), testEncryptWithPasswordRejectsUnsupportedHashAlgorithm(), testEncryptWithPasswordRejectsTagShorterThanMinimum() e testDecryptWithPasswordRejectsTruncatedTag(); testGetSimpleRandomToken() verifica la lunghezza del token con il default e con un valore personalizzatoTests/Core/HelperClasses/SessionTest.php: lo stub di Config, costruito da setConfiguration(), include i nuovi parametri e accetta valori personalizzati; aggiunto testSessionCookieAndTokenFollowConfiguration(); testStaleSessionIdWithinGracePeriodFollowsCurrentSession() diviso in testConcurrentRequestWithStaleSessionIdWithinGracePeriodJoinsCurrentSession() e testStaleSessionIdFollowsRotationChainToLatestSession(), con metodi di supporto al posto dei commentiTests/Security/HttpClasses/AuthenticationTest.php: lo stub di Config include i nomi del token e dei campi ed è passato al costruttore; aggiunti testCheckCsrfTokenUsesConfiguredKey(), testCheckAuthenticableUsesConfiguredFieldNames() e testCheckMultiFactorUsesConfiguredFieldName(), con i metodi di supporto createConfigStubWith() e createFilterAcceptingStrings()Tests/Security/HttpClasses/OAuthAuthenticationTest.php: uno stub di Config con chiave e lunghezza dello state è passato al costruttore, invece di dipendere dall'istanza globale; aggiunto testStateIsStoredAndCheckedUnderConfiguredKey(), che verifica anche la lunghezza dello stateTests/Core/HelperClasses/DispatcherTest.php: lo stub di Config include i parametri di sessione, letti da Session::start() attraverso AuthenticationTests/Console/Services/Installation/InstallationManagerTest.php: il config.php di prova dichiara il namespace del modulo; testInstallCopiesConfigFile() verifica la riscrittura del namespacedocs/configuration-reference.md: nuova sezione "Configurazione del Progetto e Valori di Default", nuove sezioni "Sessione" e "Autenticazione", nuove costanti della cifratura con password e SIMPLE_RANDOM_TOKEN_BYTES, esempi corretti come descritto sopradocs/installation.md: l'installazione manuale copia solo config.php (come configFramework.php) invece dell'intera cartella Config, e include la sostituzione del namespace nel file copiatodocs/security.md: aggiornate le sezioni "Cookie di Sessione", "Rotazione dell'ID di Sessione", "Cifratura con Password" e "Protezione CSRF", la nota per i progetti già installati, l'esempio di SIMPLE_HASH_ALGORITHM e quello di getSimpleRandomToken(); aggiunti i nomi dei campi dei form di login e di verifica a due fattoridocs/api-reference.md: aggiornate firme e formato dell'involucro di encryptWithPassword()/decryptWithPassword(); aggiunti i costruttori di Authentication e OAuthAuthentication, i nomi configurabili del token CSRF e dei campi del form, chiave e lunghezza dello state OAuth e la firma di getSimpleRandomToken(); corretta la firma di checkMultiFactor(), che non riportava il default di $dataMapperdocs-phpdoc/: rigenerate le pagine di Encryptor, Session, Authentication, OAuthAuthentication e InstallationManager e l'indice di ricercaEncryptor::PASSWORD_KEY_DERIVATION_DEFAULT_ITERATIONS, Encryptor::PASSWORD_KEY_DERIVATION_MINIMUM_ITERATIONS, Session::ROTATION_GRACE_SECONDS, Session::ROTATED_AT_KEY e Session::NEXT_SESSION_ID_KEY, introdotte nella 12.8.x, non esistono più: il codice che le referenzia solleva Error: Undefined constant. I valori si leggono da Config (passwordKeyDerivationDefaultIterations, passwordKeyDerivationMinimumIterations, sessionRotationGraceSeconds, sessionRotatedAtKey, sessionNextSessionIdKey).encryptWithPassword() e decryptWithPassword() non devono cambiare nulla, anche se passano le iterazioni per nome.configFramework.php dichiara già il namespace Config, e le costanti nuove arrivano dal modulo. Per cambiarne il valore basta aggiungerle al proprio configFramework.php.Config/config.php a mano deve sostituire namespace SismaFramework\Config; con namespace Config; (vedi docs/installation.md); lasciando il namespace del modulo, ogni costante viene dichiarata due volte e PHP emette un warning.Config/config.php del modulo: le costanti sono ora in SismaFramework\Config\ e non più in Config\. Il framework non le referenzia per nome, solo tramite Config.Error: Undefined constant e assume il valore di default del modulo.sisma-pw1 non più supportati: decryptWithPassword() restituisce false per gli involucri creati con la 12.8.x. Chi ne avesse salvati deve decifrarli con la 12.8.x e cifrarli di nuovo dopo l'aggiornamento.encryptWithPassword() produce involucri sisma-pw2, che la 12.8.x non sa decifrare: tornando a una versione precedente, gli involucri creati con la 12.9.0 restano illeggibili fino al nuovo aggiornamento.OPENSSL_CONFIG_PATH viene usato Config/openssl.cnf del modulo, con lo stesso contenuto del file temporaneo generato fino alla 12.8.x; il framework non scrive più nella cartella temporanea di sistema.
Patch che corregge tre difetti di Session: la rotazione dell'ID a ogni richiesta disconnetteva l'utente al caricamento di una normale pagina, il cookie di sessione veniva scartato dai browser sulle porte non standard, e il token anti-dirottamento veniva riscritto a ogni richiesta, rendendo isValidSession() sempre vero.
Core/HelperClasses/Session::start() — sessione persa con richieste concorrenti
Dalla patch che ha introdotto session_regenerate_id(true), ogni richiesta ruotava l'ID cancellando subito la sessione precedente. Il compromesso era documentato per i casi di tab multipli e chiamate fetch in parallelo, ma riguarda anche il caso più comune: quando fogli di stile e script passano dal front controller, il browser li richiede in parallelo con lo stesso cookie della pagina; la prima richiesta servita ruota l'ID e cancella la sessione, le altre ricevono una sessione vuota e il cookie che il browser conserva è quello dell'ultima risposta arrivata. L'utente risultava disconnesso già al caricamento di una pagina con un foglio di stile e uno script.
La rotazione a ogni richiesta resta, ma con un periodo di grazia, secondo lo schema indicato dal manuale di PHP per session_regenerate_id() e senza un session handler dedicato: rotate() crea il nuovo ID con session_regenerate_id(false), che ne eredita i dati, e marca la vecchia sessione con l'istante della rotazione e il nuovo ID (Session::ROTATED_AT_KEY, Session::NEXT_SESSION_ID_KEY). Una richiesta che si presenta con un ID già ruotato da meno di Session::ROTATION_GRACE_SECONDS (30 secondi) viene agganciata alla sessione corrente, seguendo anche più rotazioni consecutive (followRotation()), e non ruota di nuovo; oltre il periodo di grazia riceve una sessione vuota, come prima. Entrambe le sessioni esistono quando vengono riaperte, quindi la rotazione funziona anche con session.use_strict_mode attivo.
Core/HelperClasses/Session::start() — cookie scartato sulle porte non standard
Il parametro domain del cookie valeva HTTP_HOST, che su una porta diversa da 80 e 443 contiene anche la porta (localhost:8080): un attributo Domain con la porta non è valido e i browser scartavano il cookie, per cui il login non funzionava in sviluppo né in qualunque installazione su porta non standard. Anche senza porta, un Domain esplicito estende il cookie a tutti i sottodomini. domain non viene più impostato: il cookie resta legato all'host esatto che l'ha emesso.
Core/HelperClasses/Session::start() — token anti-dirottamento riscritto a ogni richiesta
start() registrava a ogni richiesta l'impronta di User-Agent e indirizzo IP del client corrente, e isValidSession() la confrontava con l'impronta della stessa richiesta: il confronto era sempre vero e non poteva rilevare una sessione usata da un altro client. Il token viene ora registrato solo alla creazione della sessione.
File modificati:
Core/HelperClasses/Session.php: start() non imposta più domain, ruota con periodo di grazia e registra il token solo se assente; aggiunti i metodi privati followRotation() e rotate() e le costanti pubbliche ROTATION_GRACE_SECONDS, ROTATED_AT_KEY e NEXT_SESSION_ID_KEYTests/Core/HelperClasses/SessionTest.php: testSessionRegenerateIdInvalidatesPreviousId() sostituito da testSessionRotationKeepsDataAlongLegitimateChain(), testStaleSessionIdWithinGracePeriodFollowsCurrentSession() e testStaleSessionIdAfterGracePeriodGetsEmptySession(); aggiunti testSessionCookieIsBoundToExactHost() e testSessionTokenIsBoundToFirstRequest(). I test sul periodo di grazia, sul cookie e sul token falliscono contro l'implementazione precedentedocs/security.md: aggiornata la sezione "Cookie di Sessione" e aggiunta la sezione "Rotazione dell'ID di Sessione"docs-phpdoc/: rigenerata la pagina di Session (nuove costanti pubbliche) e l'indice di ricercaDomain e non viene inviato ai sottodomini dell'host che l'ha emesso. Un'applicazione che condivideva la sessione tra sottodomini deve impostare domain con un override di Session.isValidSession() restituisce false quando User-Agent o indirizzo IP del client sono cambiati dall'apertura della sessione (per esempio un dispositivo mobile che cambia rete). Il framework non chiama isValidSession(): il comportamento cambia solo per le applicazioni che la usano.
Patch che permette di salvare e leggere un'entità con una proprietà cifrata facoltativa che vale null: fino alla 12.8.0 entrambe le operazioni sollevavano un TypeError.
Orm/HelperClasses/DataMapper::parseSingleProperty() — cifratura di un valore nullo
Al salvataggio, una proprietà dichiarata con addEncryptedProperty() veniva passata a Encryptor::encryptString() qualunque fosse il suo valore. Per una proprietà nullable che vale null (per esempio una chiave privata custodita altrove, quindi assente) la chiamata falliva con TypeError: Encryptor::encryptString(): Argument #1 ($plainText) must be of type string, null given, e l'entità non poteva essere salvata. Un valore null viene ora scritto come NULL senza cifratura; il vettore d'inizializzazione viene generato solo quando c'è effettivamente un valore da cifrare, e resta quello già presente se l'entità ne ha uno.
Orm/BaseClasses/BaseResultSet — decifratura di un valore nullo
Alla lettura, convertToBaseEntity() e hydrateRelatedEntity() (entità caricate con join) chiamavano Encryptor::decryptString() su ogni proprietà cifrata quando la riga aveva un vettore d'inizializzazione, anche se il valore della colonna era NULL: stesso TypeError, sollevato leggendo un'entità che aveva un'altra proprietà cifrata valorizzata. Un valore NULL viene ora restituito come null senza decifratura.
File modificati:
Orm/HelperClasses/DataMapper.php: parseSingleProperty() cifra solo i valori diversi da nullOrm/BaseClasses/BaseResultSet.php: convertToBaseEntity() e hydrateRelatedEntity() decifrano solo i valori diversi da nullTestsApplication/Entities/EntityWithNullableEncryptedProperty.php: nuova entità di test con una proprietà cifrata facoltativa e una obbligatoriaTests/Orm/HelperClasses/DataMapperTest.php: aggiunto testSaveEntityWithNullEncryptedPropertyKeepsNull(), che salva l'entità con la proprietà facoltativa nulla (scritta come NULL, vettore d'inizializzazione generato per l'altra) e poi la aggiorna valorizzandola (cifrata con lo stesso vettore); fallisce contro l'implementazione precedenteTests/Orm/ResultSets/ResultSetMysqlTest.php: aggiunto testFetchWithEntityWithNullEncryptedProperty(), che legge una riga con la colonna cifrata facoltativa a NULL e l'altra cifrata; fallisce con TypeError contro l'implementazione precedentedocs/orm.md: nuova sezione "Proprietà Cifrate" (dichiarazione, vettore d'inizializzazione obbligatorio, passphrase, valori nulli)TypeError, ora riescono. Le proprietà cifrate valorizzate sono trattate esattamente come prima.
Minor release che aggiunge a Encryptor la cifratura simmetrica con una chiave ricavata da una password, indipendente dalla passphrase dell'applicazione. Serve quando un dato conservato sul server deve restare inutilizzabile senza un segreto che conosce solo il suo titolare, per esempio una chiave privata di firma custodita sul server ma sbloccabile solo con il PIN dell'utente.
Core/HelperClasses/Encryptor — encryptWithPassword() e decryptWithPassword()
encryptString() usa ENCRYPTION_PASSPHRASE: chiunque controlli il server può decifrare. I due nuovi metodi ricavano invece la chiave dalla password passata dal chiamante:
encryptWithPassword(string $plainText, string $password, int $iterations = self::PASSWORD_KEY_DERIVATION_DEFAULT_ITERATIONS): string — ricava una chiave di 256 bit con PBKDF2-HMAC-SHA256 (sale casuale di 16 byte, 600.000 iterazioni per default) e cifra con AES-256-GCM (IV casuale di 12 byte, tag di 16 byte). Solleva InvalidArgumentException se la password è vuota o le iterazioni sono fuori dall'intervallo ammesso (da 100.000 a 10.000.000).decryptWithPassword(string $envelope, string $password): string|false — restituisce false, senza eccezioni, se la password è errata o l'involucro è malformato, di una versione o di un algoritmo non supportati, con iterazioni fuori intervallo o alterato: stesso contratto di decryptString().
Il risultato è un involucro autodescrittivo, sisma-pw1$pbkdf2-sha256$<iterazioni>$<sale>$<iv>$<tag>$<dati>, con i campi binari in base64: contiene tutto il necessario per decifrare tranne la password, quindi chi lo usa non deve conservare a parte sale e IV. L'intestazione (versione, algoritmo di derivazione, iterazioni, sale) è passata ad AES-GCM come dato autenticato aggiuntivo: alterare i parametri rende l'involucro indecifrabile invece di indebolirlo. Il limite massimo delle iterazioni impedisce che un involucro manipolato imponga una derivazione arbitrariamente costosa.
I parametri non sono letti da Config ma scritti nell'involucro, per due ragioni: un progetto già installato non riceve le nuove costanti di Config/config.php (vedi la 12.3.0), e con le costanti la prima chiamata solleverebbe Error: Undefined constant; inoltre un involucro deve restare decifrabile anche dopo che il costo di default è stato alzato. Il terzo argomento di encryptWithPassword() permette di scegliere un costo diverso per i nuovi involucri. Le costanti pubbliche PASSWORD_KEY_DERIVATION_DEFAULT_ITERATIONS e PASSWORD_KEY_DERIVATION_MINIMUM_ITERATIONS espongono i valori.
PBKDF2 e AES-GCM usano solo le estensioni hash e openssl, già richieste dal framework: nessuna nuova dipendenza (Argon2id avrebbe richiesto sodium).
File modificati:
Core/HelperClasses/Encryptor.php: aggiunti encryptWithPassword(), decryptWithPassword() e le costanti dell'involucroTests/Core/HelperClasses/EncryptorTest.php: aggiunti 9 test (round-trip con il costo di default, dati binari e Unicode, involucri diversi a parità di input, password errata o vuota, dati alterati, intestazione alterata, involucri malformati o non supportati, rifiuto di password vuota e di iterazioni insufficienti)docs/security.md: nuova sezione "Cifratura con Password"; docs/api-reference.md: aggiunti i due metodi; docs/configuration-reference.md: nota sull'assenza di costanti di configurazionedocs-phpdoc/: rigenerata la pagina di Encryptor e l'indice di ricercaMinor release che estende al query builder il supporto alle colonne JSON introdotto con la 12.6.0: condizioni, selezione e ordinamento sui valori contenuti nei documenti, con conversione del tipo dei valori gestita nell'SQL.
Orm/HelperClasses/Query — condizioni, selezione e ordinamento su documenti JSON
Con la 12.6.0 una colonna JSON poteva essere letta e scritta, ma non interrogata nel contenuto. Query espone ora sei metodi:
| Metodo | SQL generato |
|---|---|
appendJsonCondition($column, $path, $operator, $value, $jsonValueType) | JSON_UNQUOTE(JSON_EXTRACT(col, path)) <operatore> ?, con l'eventuale conversione del tipo |
appendJsonContainsCondition($column, $jsonValueType, $value, $path) | JSON_CONTAINS(col, <valore>, path) |
appendJsonPathExistsCondition($column, $path) | JSON_CONTAINS_PATH(col, 'one', path) |
appendJsonNullCondition($column, $path) | CAST(JSON_TYPE(JSON_EXTRACT(col, path)) AS BINARY) = 'NULL' |
setJsonExtractColumn($column, $path, $jsonValueType, $columnAlias, $append) | il valore estratto come colonna della SELECT |
appendOrderByJsonPath($column, $path, $indexing, $jsonValueType) | il valore estratto come criterio di ORDER BY |
Le condizioni vanno nel WHERE o nell'HAVING secondo la clausola attiva, come appendCondition(). Le funzioni usate sono disponibili sia in MySQL sia in MariaDB, che non supporta gli operatori -> e ->>, e l'SQL generato è stato verificato su MySQL 8.4 e MariaDB 11.8. In appendJsonNullCondition() il risultato di JSON_TYPE è convertito in BINARY prima del confronto: in MariaDB la sua collation e quella del letterale, derivata dalla connessione, hanno la stessa coercibilità, e il confronto diretto fallisce con l'errore 1267 (Illegal mix of collations).
$query->setWhere()
->appendJsonCondition('attributes', '$.price', ComparisonOperator::greater, Placeholder::placeholder, JsonValueType::decimal)
->appendAnd()
->appendJsonContainsCondition('attributes', JsonValueType::string, Placeholder::placeholder, '$.tags');
$bindValues = [10.5, 'sale'];
$bindTypes = [DataType::typeDecimal, DataType::typeString];
Il path si passa come stringa, inserita come letterale dopo la validazione contro la grammatica $, .chiave, [indice] (un path non conforme solleva un'AdapterException alla costruzione della query), oppure come Placeholder::placeholder, legato come parametro nella posizione in cui compare nell'SQL, prima del valore del confronto.
Orm/Enumerations/JsonValueType — tipo dei valori nei documenti
Un valore estratto da un documento è testo: un confronto > con un decimale legato come stringa sarebbe lessicografico ('9.5' > '10.5'), e il booleano JSON true estratto è la stringa 'true', diversa dall'1 con cui MySQL rappresenta un booleano SQL. Il nuovo enum (string, integer, decimal, boolean, json) indica il tipo del valore nel documento, e l'adapter genera la conversione nell'SQL: CAST(... AS SIGNED), CAST(... AS DECIMAL(65,30)) e (... = 'true') sul valore estratto, JSON_QUOTE(?), CAST(? AS CHAR) e IF(?, 'true', 'false') sul valore cercato da JSON_CONTAINS. I valori si legano con il DataType naturale, come nelle condizioni sulle colonne ordinarie. JsonValueType::json lascia il valore invariato, per cercare oggetti e array legati come SismaJson; non è ammesso nei confronti di appendJsonCondition(), dove solleva un'AdapterException.
File modificati:
Orm/Enumerations/JsonValueType.php: nuovo enumOrm/HelperClasses/Query.php: aggiunti setJsonExtractColumn(), appendOrderByJsonPath(), appendJsonCondition(), appendJsonContainsCondition(), appendJsonPathExistsCondition(), appendJsonNullCondition() e il metodo privato appendToCurrentCondition()Orm/BaseClasses/BaseAdapter.php: aggiunti escapeJsonPath(), che valida i path letterali, opJsonCondition(), che compone le condizioni di confronto, e i metodi astratti opJsonExtract(), opJsonContains(), opJsonContainsPath() e opJsonIsNull()Orm/Adapters/AdapterMysql.php: implementati i metodi astratti, con i metodi privati opJsonExtractFunction() e opJsonUnquoteFunction()Tests/Orm/Adapters/AdapterMysqlTest.php: test dell'SQL generato per ogni metodo e tipo di valore e della validazione dei pathTests/Orm/HelperClasses/QueryTest.php: test della delega all'adapter e della collocazione in WHERE, HAVING, colonne e ordinamentodocs/advanced-orm.md: nuova sezione "Query su Colonne JSON"; docs/orm.md: aggiornata la limitazione sulle query; docs/enumerations.md: aggiunto JsonValueTypeOrm/Adapters/AdapterMysql::parseBind() — booleani e array legati come typeJson
DataType::typeJson presuppone un valore già serializzato, SismaJson o stringa JSON, e lo lega come stringa. Un booleano subiva il cast di PHP, per cui true arrivava al database come '1', cioè il numero JSON 1, e false come stringa vuota, che non è JSON valido; un array non era convertibile. Questi valori sollevano ora un'AdapterException che indica il tipo ricevuto.
File modificati:
Orm/Adapters/AdapterMysql.php: parseBind() verifica il valore tramite il nuovo metodo privato checkJsonBindValue()Tests/Orm/Adapters/AdapterMysqlTest.php: aggiunti testSelectWithBooleanBoundAsJson() e testSelectWithArrayBoundAsJson()Query e l'enum JsonValueType sono nuovi; i metodi astratti sono aggiunti a BaseAdapter, classe @internal.DataType::typeJson sollevano un'AdapterException invece di essere inviati al database come valori errati.SismaJson per le Colonne JSON
Minor release che introduce nell'ORM il tipo SismaJson, per mappare le colonne JSON di MySQL e MariaDB su proprietà delle entità decodificate e tracciate dal change tracking come gli altri tipi custom.
Orm/CustomTypes/SismaJson — proprietà JSON nelle entità
Finora una colonna JSON poteva essere mappata solo su una proprietà string, lasciando all'applicazione la codifica e la decodifica e facendo rilevare come modifica qualunque differenza testuale, anche solo nell'ordine delle chiavi che MySQL normalizza in fase di salvataggio. Una proprietà tipizzata SismaJson viene ora decodificata in lettura (Parser::parseValue()) e serializzata in scrittura (Parser::unparseValue()), sia nell'ORM sia nel parsing di form e argomenti delle action, che accettano la stringa JSON o un array.
protected SismaJson $attributes;
$product->attributes = new SismaJson(['color' => 'red']);
$product->attributes = $product->attributes->with('size', 'M');
echo $product->attributes['color'];
SismaJson contiene un oggetto o un array JSON, decodificato in array associativo, e implementa ArrayAccess, IteratorAggregate, Countable e JsonSerializable. È immutabile: with() e without() restituiscono una nuova istanza, mentre l'assegnazione tramite indice solleva una LogicException. Un oggetto mutabile modificato in place non passerebbe da BaseEntity::__set() e la modifica non verrebbe salvata. equals() confronta i documenti ignorando l'ordine delle chiavi degli oggetti ma non quello degli elementi delle liste, con confronto stretto sui tipi.
Orm/Interfaces/CustomTypeInterface — interfaccia per i tipi custom confrontabili
Nuova interfaccia con il metodo equals(self $other): bool, implementata da SismaJson. Il change tracking di BaseEntity usa equals() per le proprietà il cui tipo implementa CustomDateTimeInterface o CustomTypeInterface. CustomDateTimeInterface resta invariata e non estende la nuova interfaccia: renderla figlia di CustomTypeInterface amplierebbe il parametro di equals(), e le implementazioni esistenti che lo dichiarano come CustomDateTimeInterface violerebbero la controvarianza dei parametri, con un fatal error.
DataType::typeJson e FilterType::isJsonDataType::typeJson: restituito da DataType::fromReflection() per le proprietà SismaJson e dal rilevamento automatico del tipo di binding di AdapterMysql; l'adapter lo lega come PDO::PARAM_STR.FilterType::isJson e Filter::isJson(): verificano che il valore sia un'istanza di SismaJson. FilterType::fromPhpType() lo restituisce per le proprietà SismaJson, per cui lo scaffolding genera il filtro corretto invece di fallire con un TypeError.File modificati:
Orm/CustomTypes/SismaJson.php: nuova classeOrm/Interfaces/CustomTypeInterface.php: nuova interfacciaOrm/BaseClasses/BaseEntity.php: checkCustomDateTimeInterfacePropertyChange() rinominato in checkCustomTypePropertyChange(), che tramite il nuovo metodo privato isCustomType() considera anche CustomTypeInterfaceOrm/Enumerations/DataType.php: aggiunto il case typeJson e il relativo ramo in fromReflection()Orm/Adapters/AdapterMysql.php: typeJson tradotto in PDO::PARAM_STR; parseGenericBindType() riconosce SismaJsonCore/HelperClasses/Parser.php: parseValue() gestisce SismaJson tramite il nuovo metodo parseJson(), che converte una JsonException in InvalidArgumentException e restituisce null per il letterale JSON null se la proprietà è nullable; unparseValue() serializza SismaJson con toJson()Core/Enumerations/FilterType.php: aggiunto il case isJson e il relativo ramo in fromPhpType()Core/HelperClasses/Filter.php: aggiunto isJson()TestsApplication/Entities/BaseSample.php: aggiunte le proprietà jsonWithoutInitialization, jsonWithInitialization e jsonNullableWithInitializationTests/Orm/CustomTypes/SismaJsonTest.php: nuovo testTests/Orm/BaseClasses/BaseEntityTest.php, Tests/Core/HelperClasses/ParserTest.php, Tests/Orm/Enumerations/DataTypeTest.php, Tests/Orm/Adapters/AdapterMysqlTest.php, Tests/Core/Enumerations/FilterTypeTest.php, Tests/Core/HelperClasses/FilterTest.php: test per il nuovo tipoTests/Orm/HelperClasses/DataMapperTest.php: valori e tipi di binding attesi aggiornati con le nuove proprietà di BaseSampledocs/orm.md, docs/scaffolding.md, docs/enumerations.md: documentato il nuovo tipoJsonException in SismaJson::fromJson() e una InvalidArgumentException durante l'idratazione, per cui un'entità con un JSON scalare nella colonna non può essere caricata. Fa eccezione il letterale JSON null, che su una proprietà nullable viene idratato come null, e che un successivo salvataggio della colonna riscrive come NULL SQL.{} viene riscritto come [].SismaJson cifrata viene salvata come stringa cifrata, che non è JSON valido: la colonna deve essere testuale e non JSON.JSON_EXTRACT, ->>) non sono supportate dal query builder.string mappate su colonne JSON continuano a funzionare come prima; il nuovo comportamento si attiva solo per le proprietà tipizzate SismaJson.DataType::typeJson e FilterType::isJson si aggiungono ai case esistenti. Un match esaustivo senza ramo default su DataType o FilterType scritto nel codice applicativo solleverebbe un UnhandledMatchError per i nuovi case.help() nei Comandi Console e Deprecazione di configure()
Minor release che introduce nei comandi console il metodo help(): string, che restituisce il testo d'aiuto del comando, e depreca configure(): void, che lo stampava. configure() sarà rimosso nella 13.0.0, dove help() diventerà obbligatorio: la release permette di convertire i comandi dei moduli prima dell'aggiornamento.
Console/BaseClasses/BaseCommand::help() — testo d'aiuto restituito invece che stampato
Il nome configure() richiamava una fase di preparazione del comando, sul modello di Symfony, ma per convenzione il metodo stampava soltanto il testo d'aiuto, e dalla 12.4.2 viene chiamato esclusivamente con l'opzione --help: un comando che vi inserisse una preparazione vera la vedrebbe eseguita solo con --help e mai durante l'esecuzione normale. Il nuovo metodo help(): string restituisce il testo d'aiuto, e la stampa è a carico di BaseCommand::run().
Prima:
#[\Override]
protected function configure(): void
{
$this->output(<<<OUTPUT
Usage: php SismaFramework/Console/sisma report <module>
OUTPUT);
}
Dopo:
#[\Override]
protected function help(): string
{
return <<<OUTPUT
Usage: php SismaFramework/Console/sisma report <module>
OUTPUT;
}
Con --help, run() stampa il risultato di help(). Se il comando implementa ancora soltanto configure(), run() chiama configure() come in precedenza e stampa in coda un avviso di deprecazione che indica la classe da convertire. Se il comando implementa entrambi i metodi, viene usato help().
I quattro comandi del framework (fixtures, install, scaffold, upgrade) implementano ora help().
BaseCommand::configure(): deprecato dalla versione 12.5.0, sarà rimosso nella versione 13.0.0. Implementare help(): string. Il metodo non è più astratto: BaseCommand ne fornisce un'implementazione vuota, per cui i comandi nuovi non devono dichiararlo. L'avviso di deprecazione è una riga stampata in console e non un E_USER_DEPRECATED, perché ErrorHandler::handleCommandLineInterfaceNonThrowableError() termina il processo con codice 1 a qualunque errore registrato, deprecazioni incluse.
La conversione va completata prima di aggiornare alla 13.0.0: nella 13 la console carica all'avvio le classi di tutti i comandi dei moduli, e un comando che non implementa help(), o che dichiara configure() con #[\Override], provoca un fatal error che impedisce l'esecuzione di qualunque comando sisma, compreso sisma upgrade.
File modificati:
Console/BaseClasses/BaseCommand.php: configure() non più astratto, con implementazione vuota e annotazione @deprecated; aggiunto help(): string, che restituisce una stringa vuota; run() con --help delega al nuovo metodo privato printHelp(), che usa help() o, per i comandi non convertiti, configure() seguito dall'avviso di deprecazione; il nuovo metodo privato isOverridden() verifica tramite reflection se un metodo è ridefinito dal comandoConsole/Commands/FixturesCommand.php, Console/Commands/InstallationCommand.php, Console/Commands/ScaffoldCommand.php, Console/Commands/UpgradeCommand.php: configure() convertito in help(): stringTests/Console/BaseClasses/BaseCommandTest.php: nuovo test, che copre la stampa di help() con --help senza esecuzione del comando, il ripiego su configure() con l'avviso di deprecazione, la precedenza di help() quando il comando implementa entrambi i metodi e il suggerimento in caso di fallimentoTests/Console/HelperClasses/CommandDispatcherTest.php, Tests/Console/HelperClasses/Dispatcher/CommandFactoryTest.php: i comandi fittizi implementano help(): string invece di configure()Tests/Console/Commands/UpgradeCommandTest.php, Tests/Console/Commands/InstallationCommandTest.php: testConfigureShowsHelpMessage() rinominato in testHelpOptionShowsHelpMessage() e testConfigureShowsSkipDbOption() in testHelpOptionShowsSkipDbOption()configure() continuano a funzionare senza modifiche, anche se lo dichiarano con #[\Override].--help, un comando non ancora convertito stampa, dopo il proprio testo d'aiuto, una riga Deprecated: … che indica la classe da convertire.
Patch che fa uscire il comando sisma con un codice diverso da zero quando un comando fallisce e che stampa il testo d'aiuto dei comandi solo con l'opzione --help, invece che a ogni esecuzione.
Console/sisma — codice d'uscita 0 anche quando il comando falliva
Un comando segnala il fallimento restituendo false da execute(), e il valore risaliva correttamente fino a CommandDispatcher::run(). Lo script sisma, però, ignorava il valore restituito: il processo usciva con codice 1 solo per le eccezioni non catturate, mentre un comando che gestiva l'errore (argomento mancante, opzione non valida, file non scrivibile) terminava con codice 0, come se fosse andato a buon fine. Di conseguenza make, script con set -e, catene con &&, cron e job CI non rilevavano il fallimento. Lo script esce ora con exit($commandDispatcher->run() ? 0 : 1).
Console/BaseClasses/BaseCommand::run() — il testo d'aiuto veniva stampato a ogni esecuzione
run() chiamava sempre configure() prima di execute(). Poiché per convenzione configure() stampa il blocco «Usage / Arguments / Options / Example», il testo d'aiuto compariva anche quando il comando era invocato correttamente e andava a buon fine, sporcando l'output e i log dei comandi eseguiti da cron o da script.
run() si comporta ora così:
--help chiama solo configure() e restituisce true, senza eseguire il comando;--help chiama solo execute();execute() restituisce false stampa una sola riga, Use --help for usage information., al posto dell'intero blocco d'aiuto.Console/HelperClasses/CommandDispatcher — suggerimento d'uso allineato
Il messaggio mostrato quando sisma è invocato senza argomenti indica ora sisma <command> --help per ottenere informazioni su un comando, invece di sisma <command>.
File modificati:
Console/sisma: il valore restituito da CommandDispatcher::run() determina il codice d'uscitaConsole/BaseClasses/BaseCommand.php: run() chiama configure() solo con --help e stampa un suggerimento in caso di fallimentoConsole/HelperClasses/CommandDispatcher.php: messaggio d'uso aggiornatoTests/Console/Commands/UpgradeCommandTest.php, Tests/Console/Commands/InstallationCommandTest.php, Tests/Console/Commands/ScaffoldCommandTest.php: i test sul testo d'aiuto impostano l'opzione help; InstallationCommandTest::testExecuteWithMissingProjectName() verifica ora l'assenza del blocco d'aiuto e la presenza del suggerimento; aggiunti a ScaffoldCommandTest i test testFailedExecutionShowsHelpHintWithoutUsage() e testHelpOptionDoesNotExecuteCommand(), ed estesa testSuccessfulExecution() per verificare l'assenza del testo d'aiutoconfigure() resta astratto e i comandi dei moduli non richiedono modifiche.--help. Chi lanciava un comando senza argomenti per leggerne l'uso deve ora aggiungere --help.false fa uscire sisma con codice 1 invece di 0. Gli script che invocano sisma vedono ora i fallimenti che prima venivano ignorati.Patch che rende la tipografia della barra di debug indipendente dai fogli di stile dell'applicazione, corregge l'apertura dei pannelli al primo click e rimuove dagli asset strutturali due file della barra non più utilizzati.
Structural/Templates/debugBar.tpl — la tipografia della barra di debug ereditava lo stile della pagina
Il blocco <style> del template definiva layout e colori della barra, ma nessuna proprietà tipografica: font, dimensione e interlinea erano ereditati dal body della pagina in cui la barra viene iniettata. Di conseguenza l'aspetto della barra cambiava da un'applicazione all'altra, e regole come un font-size elevato o un font decorativo sul body potevano renderla poco leggibile o alterarne l'altezza. Anche il contenuto dei pannelli (<pre>) dipendeva dalle eventuali regole dell'applicazione sui pre.
Il template dichiara ora esplicitamente:
.debug-bar: font di sistema sans-serif, font-size: 12px, line-height: 1.4;font-weight: 600 e cifre a larghezza fissa (font-variant-numeric: tabular-nums), per evitare spostamenti del layout al variare dei valori;.debug-bar-body e sui relativi <pre>: font monospace di sistema, font-size: 12px, line-height: 1.5.Structural/Templates/debugBar.tpl — il primo click su un'etichetta della barra non apriva il pannello
Lo script del template decideva se mostrare o nascondere il pannello confrontando style.display con 'none'. Al caricamento della pagina, però, il pannello è nascosto dalla regola CSS .debug-bar-body { display: none; } e non da uno stile inline, quindi style.display vale '': il confronto falliva e il primo click impostava display: none su un pannello già nascosto, senza alcun effetto visibile. Il pannello si apriva solo al secondo click. Lo stato viene ora letto con getComputedStyle(), che tiene conto anche delle regole CSS.
Structural/Assets/ — rimossi css/debugBar.css e javascript/jquery.debugBar.js
I due file erano residui della prima implementazione della barra di debug: stili e comportamento sono da tempo incorporati in debugBar.tpl (con JavaScript nativo, senza dipendenza da jQuery) e nessuna parte del framework li referenziava. DispatcherTest::testStructuralFileFopen() usava debugBar.css come file di esempio per il servizio degli asset strutturali: ora usa svg/logo.svg.
File modificati:
Structural/Templates/debugBar.tpl: aggiunte le proprietà tipografiche descritte sopra; il toggle dei pannelli usa getComputedStyle(targetDebugBarBody).display invece di targetDebugBarBody.style.displayStructural/Assets/css/debugBar.css, Structural/Assets/javascript/jquery.debugBar.js: eliminatiTests/Core/HelperClasses/DispatcherTest.php: testStructuralFileFopen() richiede /svg/logo.svg invece di /css/debugBar.css/css/debugBar.css e /javascript/jquery.debugBar.js non sono più serviti dagli asset strutturali. Non erano documentati né usati dal framework; un'applicazione che li includesse esplicitamente nel proprio layout riceverà ora un 404.common Facoltative in Localizator
Minor release che estende ai template il meccanismo di localizzazione gerarchica già usato per le viste: le chiavi della sezione templates del file di localizzazione possono ora rispecchiare, a profondità arbitraria, la struttura delle sottocartelle di Templates/, con label common condivise a ogni livello. La chiave piatta con il percorso completo del template, finora l'unica forma supportata per i template in sottocartelle, resta supportata ma è deprecata.
Core/HelperClasses/Localizator::getTemplateLocaleArray() — struttura gerarchica e label common per i template
getTemplateLocaleArray() cercava le label del template come chiave letterale all'interno di templates: per un template in una sottocartella (es. emails/welcome, la forma usata nella documentazione) la chiave doveva essere l'intero percorso ("emails/welcome": {...}), senza possibilità di condividere label tra template della stessa cartella. Il metodo segue ora lo stesso algoritmo di getPageLocaleArray(): suddivide il nome del template su /, scende di un livello nella sezione templates per ogni parte e unisce le label common incontrate a ogni livello, con precedenza al livello più specifico.
"templates": {
"common": { "signature": "Il team" },
"emails": {
"common": { "greeting": "Ciao" },
"account": {
"activation": { "subject": "Attiva il tuo account" }
}
}
}
Per emails/account/activation vengono restituite signature, greeting e subject.
La logica è estratta nel metodo privato getHierarchicalLocaleArray(), condiviso da getPageLocaleArray() e getTemplateLocaleArray().
Core/HelperClasses/Localizator — sezioni e common radice obbligatori
getPageLocaleArray() leggeva $locale['pages']['common'] senza verificarne l'esistenza, e getTemplateLocaleArray() passava $locale['templates'] direttamente ad array_key_exists(): un file di localizzazione privo di pages.common, di pages o di templates produceva un warning seguito da un TypeError. Tutti e tre sono ora facoltativi, e la loro assenza equivale a una sezione vuota.
"emails/welcome": {...} direttamente dentro templates): deprecata dalla versione 12.4.0, sarà rimossa nella versione 13.0.0. Per preservare il comportamento esistente, se il nome del template contiene / e la chiave piatta corrispondente esiste, getTemplateLocaleArray() restituisce il suo contenuto così com'è, con precedenza sulla struttura gerarchica e senza unire alcuna label common. La deprecazione è solo documentale: nessun E_USER_DEPRECATED viene sollevato a runtime, perché ErrorHandler::registerNonThrowableErrorHandler() in ambiente di sviluppo sostituisce l'output della pagina con la pagina di dettaglio per qualsiasi errore registrato, deprecazioni incluse.docs/internationalization.md — struttura del file di localizzazione allineata al codice
La guida descriveva un file di localizzazione piatto (chiavi direttamente alla radice), non corrispondente alla struttura letta da Localizator (sezioni pages e templates), dichiarava il supporto a file .php (Localizator legge solo file .json) e l'esempio di vista usava una variabile inesistente ($edit_post). Riscritte le sezioni "Come Funziona", "Creare un File di Lingua" e "Utilizzo nelle Viste"; aggiunte le sezioni "Utilizzo nei Template" (con la nota di deprecazione) e "Precedenza tra Label e Variabili".
docs/views.md e docs/conventions.md — estensioni e struttura dei file allineate al codice
docs/views.md indicava per il template di esempio l'estensione .html, mentre Templater cerca file .tpl; aggiunto inoltre un rimando alla localizzazione dei template. docs/conventions.md dichiarava il supporto a file di lingua .php; aggiunta la descrizione della struttura gerarchica di pages e templates.
docs-phpdoc/ — Rigenerazione
Rigenerata tramite composer phpdoc. Localizator è @internal e non compare nella documentazione generata, e le firme pubbliche di Templater sono invariate; la rigenerazione corregge due anomalie preesistenti:
SismaFramework-Core-HelperClasses-Encryptor.html 46 righe spurie accodate dopo </html>, residuo di una generazione precedente;files/scannerwork-*.html (5 file), generate da file PHP vuoti presenti nella cartella locale .scannerwork/ (workdir di SonarQube, ignorata da git ma non da phpDocumentor) e mai elencate nell'indice dei file. La cartella, che conteneva solo quei file vuoti e il report di un'analisi del 2023, è stata eliminata.File modificati:
Core/HelperClasses/Localizator.php: getPageLocaleArray() e getTemplateLocaleArray() delegano al nuovo metodo privato getHierarchicalLocaleArray(); aggiunto il metodo privato isDeprecatedFlatTemplateKey() per il supporto alla chiave piatta deprecataTestsApplication/Locales/it_IT.json: aggiunte label common a livello radice e di cartella in pages, e una sezione templates gerarchica su tre livelli con una chiave piatta deprecataTestsApplication/Templates/: nuova cartella con i template emails/welcome.tpl, emails/reminder.tpl ed emails/account/activation.tplTests/Core/HelperClasses/LocalizatorTest.php: aggiunti sei test su getPageLocaleArray() e getTemplateLocaleArray() con il file di localizzazione reale di TestsApplication (unione dei common di ogni livello, precedenza del livello più specifico, vista/template senza label proprie, chiave piatta deprecata)Tests/Core/HelperClasses/TemplaterTest.php: aggiunto testGenerateNestedTemplateWithHierarchicalLocale(), che genera un template annidato su tre livelli con Localizator realedocs/internationalization.md, docs/views.md, docs/conventions.md, docs-phpdoc/classes/SismaFramework-Core-HelperClasses-Encryptor.html, docs-phpdoc/files/scannerwork-*.html (eliminati): vedi soprarecordTestRunHistory non riconosciuto dalla versione installata
In 12.2.0 l'attributo cacheResult di phpunit.xml era stato sostituito con recordTestRunHistory, ma quest'ultimo è stato introdotto solo in PHPUnit 13.3.0 (che contestualmente depreca cacheResult), mentre il vincolo ^13.0 di composer.json e il composer.lock fissavano PHPUnit 13.2.2. La versione installata non riconosceva quindi l'attributo e ogni esecuzione della suite terminava con "OK, but there were issues" per un warning di validazione della configurazione, invece che con un esito pulito.
File modificati:
composer.json: vincolo phpunit/phpunit da ^13.0 a ^13.3, per impedire l'installazione di una versione che non riconosce l'attributophpunit.xml: schema di riferimento da 13.0/phpunit.xsd a 13.3/phpunit.xsdLa suite è verificata con PHPUnit 13.3.5.
getPageLocaleArray() restituisce lo stesso risultato per ogni file di localizzazione valido in precedenza.templates.common e dei common delle cartelle che lo contengono. Prima una chiave common in templates poteva esistere solo come localizzazione di un template chiamato common: solo in quel caso le sue label vengono ora estese anche agli altri template.
Patch che allinea Templater::generateTemplate() alla regola di precedenza già applicata al rendering delle view: le variabili passate dal codice applicativo prevalgono sulle label del file di localizzazione con la stessa chiave, e non viceversa.
Core/HelperClasses/Templater::generateTemplate() — le label di localizzazione sovrascrivevano le variabili passate dal codice
generateTemplate() combinava le variabili e le label restituite da Localizator::getTemplateLocaleArray() con array_merge($vars, $locale): poiché array_merge() dà precedenza al secondo array, a parità di chiave la label del file di localizzazione sostituiva la variabile passata dal chiamante. Il comportamento era l'opposto di quello di RenderService, che esegue extract() prima sulle label (getPageLocaleArray()) e poi sulle variabili, lasciando l'ultima parola allo sviluppatore. La conseguenza era duplice: non era possibile specializzare una label per un singolo utilizzo del template (es. un oggetto email personalizzato) senza modificare il file di localizzazione, valido per tutti; e l'aggiunta al file di localizzazione di una chiave omonima a una variabile dinamica (es. name, url) ne sostituiva silenziosamente il valore nel template generato.
L'ordine degli argomenti di array_merge() è stato invertito, rendendo le label valori di default sovrascrivibili dalle variabili, come per le view.
Nessun test esistente copriva generateTemplate(): TemplaterTest verificava solo parseTemplate() e l'esistenza dei metodi.
File modificati:
Core/HelperClasses/Templater.php: generateTemplate() usa ora array_merge($localizator->getTemplateLocaleArray($template), $vars) invece di array_merge($vars, $localizator->getTemplateLocaleArray($template))Tests/Core/HelperClasses/TemplaterTest.php: aggiunto testGenerateTemplateVarsOverrideLocaleLabels(), che fornisce una label in conflitto con una variabile (name) e una label senza conflitto (site) e asserisce che la prima sia sovrascritta dalla variabile e la seconda applicata; fallisce contro l'implementazione precedenteTemplater::generateTemplate() che passano una variabile con la stessa chiave di una label definita in templates nel file di localizzazione producono ora il valore della variabile anziché quello della label. Le chiamate senza chiavi in conflitto producono lo stesso output di prima.
Minor release che estende Encryptor — finora limitato ad hash e cifratura simmetrica — con un set completo di primitive di crittografia asimmetrica: coppie di chiavi, richieste di firma certificato, certificati self-signed o firmati da CA (con controllo esplicito dell'estensione X.509v3 basicConstraints), verifica della catena di fiducia, firma/verifica di dati e cifratura a busta (envelope encryption). Le nuove funzioni sono generiche e riusabili da qualunque progetto basato sul framework, non legate a un caso d'uso applicativo specifico — non è stata rilasciata una prima versione ridotta (solo self-signed) perché, trattandosi di codice di libreria condiviso e non di un caso d'uso applicativo puntuale, un set di primitive incompleto (mancava, ad esempio, il corrispettivo di "firma" per la sola verifica del certificato, o l'analogo asimmetrico di encryptString()) avrebbe significato lasciare un lavoro a metà.
Core/HelperClasses/Encryptor — dieci nuovi metodi per crittografia asimmetrica reale (OpenSSL)
Aggiunti dieci metodi statici, nello stesso stile di quelli già esistenti (?Config $customConfig = null come ultimo parametro):
Chiavi e certificati:
generateAsymmetricKeyPair(?Config $customConfig = null): array — genera una coppia di chiavi (openssl_pkey_new), ritorna ['privateKey' => string, 'publicKey' => string] in formato PEM.generateCertificateSigningRequest(string $privateKeyPem, array $distinguishedName, ?Config $customConfig = null): string — genera una CSR (openssl_csr_new) a partire da una chiave privata e un distinguished name (es. ['CN' => 'Mario Rossi', 'O' => 'Aurunci']).generateSelfSignedCertificate(string $privateKeyPem, array $distinguishedName, bool $certificationAuthority = true, ?Config $customConfig = null): string — genera una CSR e la autofirma (openssl_csr_sign con emittente null), per un soggetto che è al contempo titolare e garante del proprio certificato (root of trust). $certificationAuthority controlla l'estensione X.509v3 basicConstraints (CA:TRUE/CA:FALSE) del certificato risultante.signCertificateSigningRequest(string $certificateSigningRequestPem, string $issuerCertificatePem, string $issuerPrivateKeyPem, bool $certificationAuthority = false, ?Config $customConfig = null): string — firma la CSR di un soggetto terzo con il certificato e la chiave di un emittente (una CA), producendo un certificato con issuer ≠ subject. $certificationAuthority decide se il certificato emesso è a sua volta una CA intermedia (default: no, certificato foglia).Verifica:
signData(string $data, string $privateKeyPem, ?Config $customConfig = null): string — firma dei dati con una chiave privata (openssl_sign), ritorna la firma in base64.verifySignature(string $data, string $base64Signature, string $certificateOrPublicKeyPem, ?Config $customConfig = null): bool — verifica una firma (openssl_verify) contro un certificato o una chiave pubblica.verifyCertificateSignedByIssuer(string $certificatePem, string $issuerCertificateOrPublicKeyPem): bool — verifica la catena di fiducia (openssl_x509_verify): "questo certificato è stato davvero emesso da questo emittente?", a differenza di verifySignature() che verifica la firma su un dato applicativo, non il certificato stesso.Cifratura a busta:
encryptWithPublicKey(string $data, string $certificateOrPublicKeyPem, ?Config $customConfig = null): array — cifratura a busta (openssl_seal): la chiave pubblica cifra una chiave simmetrica generata al volo, che a sua volta cifra i dati con l'algoritmo di ENCRYPTION_ALGORITHM. A differenza della cifratura RSA diretta (openssl_public_encrypt), non ha il limite di dimensione legato alla lunghezza della chiave asimmetrica (~245 byte per RSA 2048 bit con padding PKCS1). Ritorna ['data' => string, 'envelopeKey' => string, 'initializationVector' => string], tutti in base64.decryptWithPrivateKey(array $encryptedEnvelope, string $privateKeyPem, ?Config $customConfig = null): string|false — decifra una busta prodotta da encryptWithPublicKey() (openssl_open). Ritorna false (non un'eccezione) se la decifratura fallisce, es. chiave privata errata — stesso contratto di decryptString() per lo stesso scenario.openssl.cnf di sistema richiesto
generateAsymmetricKeyPair(), generateCertificateSigningRequest(), generateSelfSignedCertificate() e signCertificateSigningRequest() passano tutte alle funzioni OpenSSL sottostanti l'opzione config, risolta da resolveOpensslConfigPath(): se Config::opensslConfigPath non è valorizzata, viene generato — una sola volta per processo, tramite tempnam(), ripulito a fine richiesta con register_shutdown_function() — un file di configurazione OpenSSL minimale (costante privata Encryptor::MINIMAL_OPENSSL_CONFIG) contenente le sezioni [req]/[req_distinguished_name] necessarie alle operazioni di base e le sezioni [v3_ca]/[v3_leaf] (rispettivamente basicConstraints critical,CA:TRUE/CA:FALSE) usate da generateSelfSignedCertificate()/signCertificateSigningRequest() per l'estensione X.509v3. Questo elimina la dipendenza da un openssl.cnf di sistema risolvibile automaticamente (assente per default su alcune installazioni PHP su Windows, dove altrimenti openssl_pkey_new()/openssl_csr_new()/openssl_csr_sign() falliscono silenziosamente ritornando false) — OPENSSL_CONFIG_PATH resta disponibile come override esplicito solo per esigenze avanzate (es. un provider/engine OpenSSL specifico).
Nuove proprietà di Config/costanti di Config/config.php (sezione "Encryptor Constants"):
| Costante | Proprietà Config | Tipo | Default |
|---|---|---|---|
ASYMMETRIC_KEY_TYPE | asymmetricKeyType | int | OPENSSL_KEYTYPE_RSA |
ASYMMETRIC_KEY_BITS | asymmetricKeyBits | int | 2048 |
ASYMMETRIC_DIGEST_ALGORITHM | asymmetricDigestAlgorithm | string | "sha256" |
CERTIFICATE_VALIDITY_DAYS | certificateValidityDays | int | 3650 |
OPENSSL_CONFIG_PATH | opensslConfigPath | string | "" (via getenv('OPENSSL_CONFIG_PATH')) |
File modificati:
Core/HelperClasses/Encryptor.php: aggiunti i dieci metodi pubblici e i metodi privati di supporto resolveOpensslConfigPath()/getMinimalOpensslConfigPath()/buildOpensslOptions()/createCertificateSigningRequestResource()Core/HelperClasses/Config.php: aggiunte le cinque proprietà readonly sopra elencateConfig/config.php: aggiunte le cinque costanti sopra elencateTests/Core/HelperClasses/EncryptorTest.php: aggiunti 21 test (incl. verifica delle estensioni basicConstraints per entrambi i booleani su entrambi i metodi, catena di fiducia positiva/su certificato self-signed/negativa, round-trip di cifratura a busta con payload oltre il limite RSA diretto, fallimento con chiave privata errata) — l'intera suite passa senza necessità di impostare OPENSSL_CONFIG_PATH, confermando l'autosufficienza della nuova configurazione minimalegenerateSelfSignedCertificate() e signCertificateSigningRequest() guadagnano il parametro bool $certificationAuthority prima di ?Config $customConfig. Non è una breaking change verso versioni precedenti pubblicate — è la forma con cui questi due metodi vengono introdotti in questa release.Config/configFramework.php viene copiato da Config/config.php una sola volta, in fase di installazione (InstallationManager::copyConfigFolder()), e non viene mai risincronizzato automaticamente da un successivo aggiornamento del framework via composer. Un progetto già installato che aggiorna la dipendenza continua a funzionare senza modifiche — le nuove proprietà di Config sono risolte in modo lazy e nessun codice esistente le referenzia. Per poter usare i nuovi metodi di Encryptor, è necessario aggiungere manualmente le cinque costanti sopra elencate al proprio Config/configFramework.php: in assenza, la prima chiamata a uno dei nuovi metodi solleva Error: Undefined constant.Config/config.php copiato da InstallationManager::copyConfigFolder(), nessuna azione richiesta.
Patch che corregge un difetto di DependentModel::buildPropertiesConditions(), il metodo che traduce le proprietà passate ai finder magici generati dal framework (findByProperties(), le varianti countBy...()/deleteBy...() generate da __callStatic/__call, ecc.) in condizioni SQL: per una proprietà foreign key (dichiarata come sottoclasse di ReferencedEntity) valorizzata con null, il metodo generava una condizione sulla colonna sbagliata, priva del suffisso Id richiesto dalla convenzione di naming dell'ORM.
Orm/ExtendedClasses/DependentModel::buildPropertiesConditions() — ricerca per foreign key null risolta sulla colonna sbagliata
Il quarto parametro di Query::appendCondition() (bool $foreignKey) indica all'adapter se il nome colonna va risolto con il suffisso Id (NotationManager::convertPropertyNameToColumnName($name, true), es. relatedEntity → related_entity_id) invece che come proprietà scalare (relatedEntity → related_entity). buildPropertiesConditions() determinava questo flag con $propertyValue instanceof ReferencedEntity — un controllo sul valore a runtime, non sul tipo dichiarato della proprietà. Per qualunque proprietà foreign key interrogata con valore null (il caso d'uso esplicito di una ricerca "dove la relazione è assente"), null instanceof ReferencedEntity è sempre false, indipendentemente dal tipo dichiarato della proprietà sull'entità: il metodo trattava quindi la colonna come se fosse scalare, generando una condizione WHERE related_entity IS NULL invece di WHERE related_entity_id IS NULL — una colonna che nella tabella non esiste, con conseguente errore SQL a runtime per qualunque ricerca su una foreign key nullable valorizzata a null.
buildPropertiesConditions() determina ora se una proprietà è una foreign key leggendo il tipo dichiarato tramite ReflectionProperty::getType() e is_subclass_of(..., ReferencedEntity::class), indipendentemente dal valore effettivo — corretto sia per il ramo null che per quello non-null, ed eliminando la necessità di istanziare una seconda volta ReflectionProperty nel ramo non-null (già usata per calcolare $bindTypes).
Nessun test esistente copriva questo percorso: gli unici test che invocano un finder con valore null su una proprietà foreign key (testMagicMethodCountByNullableEntityWithNullValue, testMagicMethodCountByMultiplePropertiesWithNullValues) verificano solo che appendCondition() venga chiamato, senza asserire il valore del quarto parametro.
File modificati:
Orm/ExtendedClasses/DependentModel.php: buildPropertiesConditions(), il rilevamento foreign key usa ora is_subclass_of($reflectionProperty->getType()->getName(), ReferencedEntity::class) invece di $propertyValue instanceof ReferencedEntity, in entrambi i rami (null e non-null)Tests/Orm/ExtendedClasses/DependentModelTest.php: aggiunto testBuildPropertiesConditionsPassesCorrectFourthParameterForNullForeignKeyValue(), che invoca un finder con null su una proprietà ?ReferencedEntity e asserisce che il quarto parametro di appendCondition() sia true; fallisce contro l'implementazione precedentebuildPropertiesConditions() è protected e non fa parte del contratto pubblico consumato direttamente dalle applicazioni; nessuna firma è cambiata.null (es. countByRelatedEntityWithInitialization(null) su una proprietà ?ReferencedEntity), che prima fallivano con un errore SQL per colonna inesistente, ora restituiscono il risultato corretto.
Minor release che rinomina due metodi pubblici di Localizator i cui nomi non riflettevano più il tipo di ritorno effettivo (e, nel caso di getComposedEnumerationLocale(), erano diventati troppo simili al metodo privato sottostante), corregge un bug introdotto in 12.1.0, e aggiunge un nuovo metodo per leggere un singolo attributo da un valore di localizzazione composto.
Core/HelperClasses/Localizator — Rinominati getEnumerationLocaleArray() e getComposedEnumerationLocaleArray()
Entrambi i nomi risalivano a un paradigma precedente (commit 20eb7a15, dic 2024) in cui il primo metodo restituiva davvero un array; da allora restituisce una singola stringa (la label), ma il nome non era mai stato aggiornato. Con l'introduzione in 12.1.0 di un secondo metodo per il valore composto, la coppia di nomi (getEnumerationLocaleArray() → stringa, getComposedEnumerationLocaleArray() → array) era diventata fuorviante. Rinominati per riflettere il tipo di ritorno effettivo:
getEnumerationLocaleArray(): string → getEnumerationLocaleLabel(): stringgetComposedEnumerationLocaleArray(): array → getComposedEnumerationLocale(): array
Il secondo nome evita deliberatamente la parola "Field/Fields": il metodo privato sottostante, getEnumerationLocaleField(), esegue il lookup grezzo (string|array) per un case dell'enum, mentre il metodo pubblico pretende e garantisce la forma composta (array). Chiamarlo getEnumerationLocaleFields() — come nome scelto in un primo momento — li rendeva praticamente indistinguibili a colpo d'occhio pur avendo contratti diversi (uno può restituire una stringa, l'altro lancia un'eccezione se non trova un array).
File modificati:
Core/HelperClasses/Localizator.php: rinominati i due metodiCore/Traits/SelectableEnumeration.php: getFriendlyLabel() aggiornato per chiamare getEnumerationLocaleLabel()Tests/Core/HelperClasses/LocalizatorTest.php: test di esistenza metodo aggiornati ai nuovi nomiCore/HelperClasses/Localizator::getEnumerationLocaleLabel() — TypeError quando il valore di localizzazione dell'enum è un array
Da quando, in 12.1.0, getEnumerationLocaleField() è diventato string|array per supportare le chiavi di localizzazione composte, questo metodo (allora getEnumerationLocaleArray()) — dichiarato : string — continuava a restituire il valore così com'era. Se il case dell'enum ha un valore locale composto (es. {"label": "...", "description": "..."}), la chiamata sollevava TypeError: Return value must be of type string, array returned.
Il metodo ora verifica il tipo del valore restituito da getEnumerationLocaleField() e, se è un array, estrae la chiave 'label'.
File modificati:
Core/HelperClasses/Localizator.php: gestione esplicita del caso array estraendo $field['label']Core/HelperClasses/Localizator::getComposedEnumerationLocale() — Errore nativo poco chiaro quando l'enum non ha un valore locale composto
Stesso difetto del punto precedente, presente fin dall'introduzione del metodo in 12.1.0 (allora getComposedEnumerationLocaleArray()): dichiarato : array, restituiva getEnumerationLocaleField() senza verificarne il tipo. Se il case dell'enum ha un valore locale semplice (stringa), la chiamata sollevava un TypeError a carico del tipo di ritorno.
Il metodo ora verifica il tipo del valore restituito da getEnumerationLocaleField() e, se non è un array, solleva una LocalizatorException esplicita — già prevista nel framework (Core/Exceptions/LocalizatorException.php) ma finora mai utilizzata — invece del TypeError/"illegal string offset" nativo di PHP.
File modificati:
Core/HelperClasses/Localizator.php: getComposedEnumerationLocale() gestisce esplicitamente il caso non-arrayCore/HelperClasses/Localizator::getEnumerationLocaleAttribute() — Lettura di un singolo attributo da un valore di localizzazione composto
Aggiunto un nuovo metodo pubblico che, dato un enum e il nome di un attributo, restituisce direttamente il valore di quell'attributo dal campo di localizzazione composto, senza dover passare per getComposedEnumerationLocale() e indicizzare manualmente l'array risultante. Internamente delega a getComposedEnumerationLocale(), ereditandone la LocalizatorException quando l'enum non ha un valore locale composto.
Il tipo di ritorno è string|array e non mixed: i file di locale sono file di linguaggio, il cui unico scopo è contenere testo (eventualmente organizzato in strutture annidate, come si vede in Aurunci/Application/Locales/it_IT.json con voci a più livelli, es. nobleQualification.don.male.label); un numero o un booleano al loro interno sarebbe un errore di contenuto, non un caso d'uso legittimo da rappresentare nel tipo. Poiché il framework non dichiara strict_types da nessuna parte, un simile errore verrebbe comunque coercito silenziosamente a stringa da PHP anziché sollevare un TypeError; il vantaggio pratico del tipo esplicito è invece sul lookup di una chiave $attribute inesistente, che con string|array (non nullable) fallisce subito con un TypeError invece di propagarsi silenziosamente come null.
File modificati:
Core/HelperClasses/Localizator.php: aggiunto getEnumerationLocaleAttribute(\UnitEnum $enumeration, string $attribute): string|arraydocs-phpdoc/ — Rigenerazione completa
Localizator è marcata @internal, quindi phpDocumentor non genera per essa (né per le altre classi @internal del framework) una pagina pubblica: le modifiche di questa release non compaiono nella documentazione generata. La rigenerazione ha comunque aggiornato 14 pagine rimaste indietro rispetto a modifiche di release precedenti mai propagate alla documentazione (es. Session per la 12.1.1, MultipleSelfReferencedEnumeration per la 12.1.2).
phpunit.xml — Sostituito l'attributo deprecato cacheResult
PHPUnit 13 segnala cacheResult come deprecato e ne annuncia la rimozione in PHPUnit 14 (vendor/phpunit/phpunit/src/TextUI/Configuration/Xml/Loader.php), in favore di recordTestRunHistory (stesso significato, nuovo nome). Sostituito nell'attributo radice <phpunit>.
File modificati:
phpunit.xml: cacheResult="true" → recordTestRunHistory="true"Localizator è marcata @internal (non è API consumabile dall'esterno del framework, coerentemente con il trattamento già riservato in passato a classi come Submittable in 11.6.0 e TransactionManager/QueryExecutor in 10.0.5); il rename dei due metodi è quindi documentato come modifica interna, non come breaking change.getComposedEnumerationLocale() o getEnumerationLocaleAttribute() su un enum privo di valore locale composto ora solleva LocalizatorException invece di un TypeError/warning nativo di PHP — un errore più esplicito, non un nuovo modo di fallire.SelectableEnumeration::getFriendlyLabel()) è stato aggiornato nello stesso commit.getEnumerationLocaleAttribute() è puramente additivo.
Patch che corregge un fatal error latente nel trait MultipleSelfReferencedEnumeration: il metodo getChoiceByMultipleParent() dichiara il parametro Language $language, ma il file non importava la classe SismaFramework\Core\Enumerations\Language.
Core/Traits/MultipleSelfReferencedEnumeration::getChoiceByMultipleParent() — Language referenziata senza use, fatal error a runtime
Il parametro Language $language viene risolto da PHP, in assenza di un use esplicito, nel namespace corrente del file (SismaFramework\Core\Traits). Poiché la classe Language vive in SismaFramework\Core\Enumerations, qualsiasi enum concreto che utilizza il trait e invoca getChoiceByMultipleParent() solleva un Error: Class "SismaFramework\Core\Traits\Language" not found. Il difetto era presente fin dall'introduzione della firma tipizzata del metodo e non è mai emerso perché nessun test in Tests/ esercita questo trait.
Aggiunta la dichiarazione use SismaFramework\Core\Enumerations\Language; in testa al file.
File modificati:
Core/Traits/MultipleSelfReferencedEnumeration.php: aggiunto use SismaFramework\Core\Enumerations\Language;
Patch che raggruppa un insieme di correzioni alla classe Session, emerse da una revisione mirata della gestione delle sessioni nel framework: un bug di ricorsione in appendItem() che ne vanificava silenziosamente il comportamento sulle chiavi annidate a più livelli, un disallineamento tra la durata del cookie di sessione e quella dei dati lato server, un confronto non timing-safe del token anti-hijacking, e una rotazione dell'ID di sessione ad ogni richiesta che non invalidava mai realmente l'ID precedente.
Core/HelperClasses/Session::appendItemRecursive() — l'append su chiavi annidate a più livelli degradava silenziosamente a un overwrite
Per una chiave con un solo livello di annidamento (es. test[one]), appendItem() funziona correttamente: verifica se il valore è già presente nell'array di destinazione e, in caso contrario, lo accoda. Per una chiave con due o più livelli (es. test[one][two]), il ramo ricorsivo di appendItemRecursive() richiamava però setItemRecursive() invece di se stesso: il risultato, per qualunque profondità superiore a uno, era che il valore più profondo veniva sovrascritto come farebbe setItem() — perdendo l'array esistente, il controllo dei duplicati e la semantica di append che il metodo promette dalla sua firma. L'unico test esistente per l'append su chiavi annidate copriva un solo livello di profondità, motivo per cui il difetto è rimasto silente.
appendItemRecursive() richiama ora se stessa nel ramo ricorsivo, esattamente come già fanno setItemRecursive(), unsetItemRecursive(), getItemRecursive() e hasItemRecursive().
File modificati:
Core/HelperClasses/Session.php: appendItemRecursive(), la chiamata ricorsiva richiama ora appendItemRecursive() invece di setItemRecursive()Tests/Core/HelperClasses/SessionTest.php: aggiunto testSessionAppendDeeplyNestedItem(), che esercita appendItem() su una chiave a due livelli di annidamento e fallisce contro l'implementazione precedenteCore/HelperClasses/Session::start() — la sessione lato server poteva scadere prima del cookie del client
Il cookie di sessione viene emesso con lifetime: 3600 (un'ora), ma nessun punto del framework allineava session.gc_maxlifetime — il parametro PHP che determina quando i dati di sessione diventano eleggibili per la garbage collection — a quello stesso valore. Con l'ini di default di molte distribuzioni (tipicamente 1440 secondi, 24 minuti), i dati di sessione potevano essere ripuliti dal server ben prima della scadenza dichiarata al client: l'utente restava con un cookie ancora "valido" ma privo di sessione corrispondente, con un logout inatteso e difficile da riprodurre perché dipendente dalla configurazione dell'hosting.
start() imposta ora esplicitamente ini_set('session.gc_maxlifetime', 3600), in modo che la vita dei dati lato server sia sempre coerente con quella del cookie, indipendentemente dalla configurazione di php.ini dell'ambiente di destinazione.
File modificati:
Core/HelperClasses/Session.php: start(), aggiunta ini_set('session.gc_maxlifetime', 3600) prima di session_set_cookie_params()Core/HelperClasses/Session::isValidSession() — confronto del token incoerente rispetto a start() e non timing-safe
start() calcola il token anti-hijacking con ($request->server['HTTP_USER_AGENT'] ?? null) . ($request->server['REMOTE_ADDR'] ?? null), tollerando l'assenza di uno dei due header. isValidSession() eseguiva lo stesso calcolo senza l'operatore di null coalescing, sollevando un warning "Undefined array key" nei contesti in cui uno dei due valori non è presente (client senza User-Agent, proxy che lo rimuovono, ambienti di test). Il confronto finale tra token atteso e token calcolato, inoltre, usava === anzichè un confronto a tempo costante, esponendo — sebbene in modo marginale, trattandosi di un hash SHA-512 non direttamente controllabile da un attaccante — a un timing attack in stile OWASP su una funzione dedicata proprio a rilevare sessioni dirottate.
isValidSession() è ora allineato a start() sull'uso di ?? null, e il confronto finale utilizza hash_equals() invece di ===.
File modificati:
Core/HelperClasses/Session.php: isValidSession(), allineato l'uso di ?? null a start() e sostituito il confronto === con hash_equals()Core/HelperClasses/Session::start() — l'ID di sessione precedente restava valido dopo la rotazione
start() viene invocato dal front controller (Public/index.php) ad ogni richiesta, e rigenera sempre l'ID di sessione tramite session_regenerate_id(). Chiamato senza l'argomento delete_old_session, però, il vecchio ID non viene invalidato: resta pienamente utilizzabile fino a quando non interviene la garbage collection di PHP (probabilistica, e ora fino a un'ora di distanza per effetto dell'allineamento di gc_maxlifetime sopra descritto). La rotazione ad ogni richiesta è una difesa in profondità riconosciuta contro il furto di sessione — riduce la finestra in cui un ID sottratto resta utile — ma senza l'invalidazione immediata del vecchio ID quel beneficio non si realizzava: l'ID rubato restava valido esattamente quanto se la rotazione non fosse mai avvenuta.
start() chiama ora session_regenerate_id(true), invalidando immediatamente l'ID precedente ad ogni rotazione.
File modificati:
Core/HelperClasses/Session.php: start(), session_regenerate_id() → session_regenerate_id(true)Tests/Core/HelperClasses/SessionTest.php: aggiunto testSessionRegenerateIdInvalidatesPreviousId(), che verifica sia la continuità dei dati lungo una catena legittima di rotazioni sia l'effettiva invalidazione di un ID già ruotato viaCore/HelperClasses/Session — preg_match_all() eseguito anche per chiavi semplici senza annidamento
setItem(), getItem(), appendItem(), unsetItem() e hasItem() eseguivano incondizionatamente preg_match_all("/\\[([^\\]]*)\\]/", $key, $matches) per rilevare la sintassi di chiave annidata (foo[bar][baz]), anche quando la chiave passata era una semplice stringa priva di parentesi quadre — il caso di gran lunga più comune nell'uso tipico della classe.
Ciascuno dei cinque metodi esegue ora preg_match_all() solo se la chiave contiene effettivamente [, evitando il costo della regex nel percorso comune.
File modificati:
Core/HelperClasses/Session.php: setItem(), getItem(), appendItem(), unsetItem(), hasItem() — preg_match_all() eseguito solo se str_contains($key, '[')session_regenerate_id(true), una richiesta concorrente che si presenta con un cookie già superato da una rotazione precedente (tab multipli, chiamate fetch/AJAX in parallelo dallo stesso client) riceve ora una sessione vuota invece di continuare silenziosamente a leggere i dati residui del vecchio ID. È il prezzo esplicito del beneficio di sicurezza descritto sopra; un'implementazione con periodo di grazia (mappatura vecchio→nuovo ID per una finestra di pochi secondi) richiederebbe un session handler dedicato e non rientra in questa patch.appendItem() su chiavi annidate a più livelli: chi si affidava (involontariamente) al comportamento precedente per sovrascrivere un valore annidato tramite appendItem() invece di setItem() vedrà ora la semantica di append effettivamente applicata.session.gc_maxlifetime: ora forzato a 3600 secondi per l'intera esecuzione dello script da Session::start(); se l'applicazione ospitante si affida a un valore diverso per scopi indipendenti dalla sessione del framework, questa impostazione lo sovrascrive.
Minor release che estende Localizator con un nuovo metodo dedicato alla lettura di valori di localizzazione strutturati (array) per i case degli enum, mantenendo getEnumerationLocaleArray() invariato per l'uso corrente (label singola come stringa).
Core\HelperClasses\Localizator::getComposedEnumerationLocaleArray() — Lettura di chiavi di localizzazione composte per enum
Introdotto un nuovo metodo pubblico per leggere, per un case di enum, un valore di localizzazione strutturato come array (es. {"label": "...", "description": "..."}), accanto al metodo esistente getEnumerationLocaleArray() che continua a restituire una singola stringa.
Motivazione: getEnumerationLocaleArray() è tipizzato string ed è usato da SelectableEnumeration::getFriendlyLabel() per costruire le choice delle select — un ritorno string|array avrebbe costretto ogni chiamante a discriminare il tipo. Un metodo dedicato, sempre array, evita questo problema e resta esplicito su cosa aspettarsi.
Implementazione:
$locale['enumerations'][lcfirst($enumerationName)][$enumeration->name]) è stata estratta nel metodo privato getEnumerationLocaleField(): string|arraygetEnumerationLocaleArray(): string e getComposedEnumerationLocaleArray(): array delegano entrambi a getEnumerationLocaleField()File modificati:
Core\HelperClasses\Localizator.php: aggiunto getComposedEnumerationLocaleArray() e il metodo privato getEnumerationLocaleField()Tests\Core\HelperClasses\LocalizatorTest — Copertura nuovo metodo
Aggiunto testGetComposedEnumerationLocaleArrayMethodExists(), sullo stesso modello del test esistente per getEnumerationLocaleArray().
getEnumerationLocaleArray() non cambia firma né comportamento; il nuovo metodo è puramente additivo.Sample/Locales/it_IT.json) restano validi; il nuovo metodo va usato solo quando la chiave dell'enum è effettivamente un array nel JSON.
Patch che corregge il contratto del trait SelfReferencedEnumeration, il cui metodo astratto getParent() era dichiarato con tipo di ritorno non nullable (self), impedendo di rappresentare correttamente la radice di una gerarchia di case enum.
Core/Traits/SelfReferencedEnumeration::getParent() — impossibile rappresentare l'assenza di un padre
Un enum che implementa il trait per modellare una struttura gerarchica (analoga a una tabella auto-referenziata) ha sempre almeno un case radice privo di padre. Con getParent(): self, quel case è costretto a restituire un valore fittizio — tipicamente se stesso o un altro case a caso — perché il tipo di ritorno non ammette null. Il problema è lo stesso, concettualmente, di una foreign key auto-referenziata dichiarata NOT NULL.
Il metodo astratto è ora dichiarato getParent(): ?self, permettendo alle implementazioni di restituire null per il case radice.
File modificati:
Core/Traits/SelfReferencedEnumeration.php: getParent(), tipo di ritorno da self a ?selfdocs/traits.md: esempio aggiornato per restituire null sul case radice invece di un riferimento fittizio a se stessogetParent(): self restano override validi di getParent(): ?self (un tipo più stretto è sempre accettato). Nessuna implementazione esistente smette di funzionare; solo le nuove implementazioni (o quelle corrette per gestire la radice) potranno dichiarare ?self e restituire null.
Patch che corregge un difetto architetturale introdotto con la conversione di Debugger da classe statica a classe di istanza (11.0.0), per cui la maggior parte dei punti che dipendono da Debugger — BaseForm, BaseController, Dispatcher, ControllerFactory, RenderService::generateView() e BaseAdapter — riceveva silenziosamente un'istanza nuova e isolata (tramite il valore di default new Debugger()) invece dell'istanza effettivamente in uso per la richiesta corrente (creata in Public/index.php e propagata tramite Dispatcher/ControllerFactory fino al controller).
Core/HelperClasses/Debugger — nessun punto di accesso condiviso all'istanza della richiesta corrente
Ogni classe che dipende da Debugger lo riceve come parametro di costruttore/metodo con valore di default new Debugger() (reso possibile dal supporto di PHP 8.1 a new negli inizializzatori). Quando il chiamante omette il parametro — la norma, non l'eccezione, in tutto il codice applicativo esistente: nessun esempio in Sample/, TestsApplication/, nei template di scaffolding o nella documentazione passa esplicitamente l'istanza corretta a RenderService::generateView() — ciascuna di queste classi finisce con una propria istanza isolata. BaseController riceve correttamente l'istanza condivisa tramite ControllerFactory (che la inietta esplicitamente via reflection), ma quell'istanza non arriva mai a RenderService::generateView() (dove la debug bar viene effettivamente generata), né a BaseAdapter/DataMapper (dove vengono conteggiate le query eseguite), né a BaseForm (dove isValid() chiama setFormFilter()), perché nessuno di questi punti la passa esplicitamente. L'effetto pratico, mai notato perché silenzioso: la debug bar non ha mai mostrato dati reali in nessuno scenario realistico.
Debugger espone ora getInstance()/setInstance()/resetInstance(), sul modello già in uso in Config e RenderService. I punti di wiring elencati sotto sostituiscono il parametro Debugger $debugger = new Debugger() con ?Debugger $debugger = null e delegano il fallback a Debugger::getInstance() nel corpo del metodo — necessario perché PHP consente new ma non chiamate a metodi statici arbitrari come valore di default di un parametro. La dependency injection resta intatta: chi passa già un'istanza esplicita (o un mock, nei test) continua a poterlo fare esattamente come prima.
File modificati:
Core/HelperClasses/Debugger.php: aggiunti i metodi statici getInstance(), setInstance(), resetInstance()Public/index.php: $debugger = Debugger::getInstance() al posto di new Debugger()Core/HelperClasses/Dispatcher.php: parametro $debugger reso nullable con fallback a Debugger::getInstance()Core/HelperClasses/Dispatcher/ControllerFactory.php: idemCore/BaseClasses/BaseController.php: idem (rimossa la constructor property promotion su $debugger per consentire il fallback)Core/BaseClasses/BaseForm.php: idemCore/Services/RenderService.php: generateView(), idemOrm/BaseClasses/BaseAdapter.php: idemTests/Core/HelperClasses/DebuggerTest.php: aggiunti testGetInstanceReturnsSameInstance(), testSetInstanceAllowsInjectingCustomInstance(), testResetInstanceCreatesNewInstanceOnNextGet(); setUp()/tearDown() resettano l'istanza singletonTests/Core/BaseClasses/BaseFormTest.php, Tests/Core/BaseClasses/BaseControllerTest.php, Tests/Core/HelperClasses/DispatcherTest.php: setUp()/tearDown() chiamano Debugger::resetInstance() per evitare che lo stato del singleton si propaghi da un test al successivoDebugger (con default new Debugger()) a ?Debugger (con default null) — un allargamento del tipo accettato, non una restrizione: chi passava già un'istanza esplicita continua a farlo senza modifiche. I tre nuovi metodi statici su Debugger sono puramente additivi.developmentEnvironment a true) mostra ora query eseguite, errori di validazione dei form e variabili di vista realmente accumulati durante la richiesta, laddove prima veniva sempre generata da un'istanza vuota. Nessun impatto in produzione.Public/index.php); non ci sono implicazioni in ambienti a processo persistente (Swoole, RoadRunner), che il framework non supporta attualmente.
Patch che corregge un difetto di Filter::customFilter(), l'unico metodo della classe Filter che passava il valore da validare direttamente a una funzione nativa di stringa (preg_match()) senza verificarne preventivamente il tipo, a differenza di tutti gli altri metodi (isString(), isEmail(), isSecurePassword(), ecc.) che già gestiscono in modo uniforme i valori non stringa restituendo false.
Core/HelperClasses/Filter::customFilter() — valori non stringa passati direttamente a preg_match()
customFilter() dichiara $value come mixed ma lo passa senza controlli al parametro $subject di preg_match(), tipizzato internamente come string non nullable. Quando questo filtro viene collegato tramite FilterType::customFilter a un campo opzionale di un BaseForm (ad esempio uno slug che rispetta un pattern di caratteri consentiti), FormValidator valorizza il dato a validare con null per un campo lasciato vuoto e lo passa comunque al filtro: preg_match() riceve quindi direttamente null, che PHP 8.1+ segnala come deprecato e può risultare in un comportamento non controllato, invece del fallimento di validazione pulito (false) che ci si aspetterebbe in analogia con gli altri metodi della classe.
customFilter() ora verifica esplicitamente is_string($value) prima di invocare preg_match(), e confronta l'esito con === 1 invece di affidarsi alla conversione implicita int→bool del valore restituito da preg_match() (che vale anche false in caso di espressione regolare non valida). Qualunque valore non stringa, incluso null, produce ora un fallimento di validazione esplicito e coerente con il resto della classe.
File modificati:
Core/HelperClasses/Filter.php: customFilter(), aggiunta guardia is_string($value) e confronto stretto === 1 sul risultato di preg_match()Tests/Core/HelperClasses/FilterTest.php: testCustomFilter(), aggiunta copertura per valori non stringa (null, int, bool, array)mixed $value, string $regularExpression). L'unico cambiamento di comportamento osservabile riguarda i valori non stringa passati a customFilter(): prima raggiungevano preg_match() così come sono, ora restituiscono false in modo esplicito, coerentemente con isString() e gli altri metodi di validazione della classe.
Patch che corregge un difetto nell'inoltro delle richieste verso i controller che implementano CallableController (routing dinamico via __call()), per cui la porzione di slug numerica-con-trattini di un URL veniva irrimediabilmente corrotta prima di raggiungere il metodo magico del controller.
Core/HelperClasses/Dispatcher::executeCallableController() — slug con parti numeriche separate da trattino decodificati in modo errato
executeCallableController() invocava sempre il controller usando parsedAction, la versione dello slug convertita in camelCase da NotationManager::convertToCamelCase() (pensata per i nomi di metodo reali, non per i segmenti di uno slug arbitrario). Per uno slug come slug-with-2000-01-01-day, la conversione produce slugWith20000101Day: i trattini tra cifre vengono rimossi senza lasciare alcun separatore, rendendo impossibile ricostruire a valle il valore originale (2000-01-01 diventa indistinguibile da 20000101). Poiché il controller non definisce un metodo reale con quel nome, la chiamata ricadeva su __call(), che riceveva quindi il nome già corrotto.
executeCallableController() ora chiama il controller con parsedAction solo se corrisponde a un metodo realmente definito sulla classe (ReflectionClass::hasMethod()); altrimenti utilizza pathAction, il segmento di URL originale, non trasformato — lo stesso valore già usato da isCallableController() per la verifica di compatibilità tramite checkCompatibility(), ripristinando la coerenza tra i due controlli.
File modificati:
Core/HelperClasses/Dispatcher.php: executeCallableController() sceglie tra parsedAction e pathAction in base a ReflectionClass::hasMethod()Tests/Core/HelperClasses/DispatcherTest.php: aggiornate le asserzioni di testSimpleSlug, testGerarchicSlug, testMultipleGerarchicSlug sul valore atteso (slug non più convertito in camelCase); aggiunto testSlugWhithNumericPart a copertura della regressioneTestsApplication/Controllers/SlugController.php: checkCompatibility(), aggiunto il caso di slug con parte numericaCallableController che si affidano a __call(), il valore ricevuto come nome del metodo cambia da una versione camelCase corrotta al segmento di URL originale — un fix di comportamento, non una modifica di contratto pubblico.
Patch che corregge un difetto della procedura di installazione (sisma install) per cui le cartelle scrivibili a runtime (Cache, Logs, filesystemMedia) non ricevevano mai i permessi di scrittura previsti dal codice, lasciando l'applicazione priva di accesso in scrittura subito dopo l'installazione.
Console/Services/Installation/InstallationManager — permessi 0777 su Cache, Logs, filesystemMedia mai applicati
install() chiama createProjectStructure(), che creava già tutte e cinque le cartelle previste (Config, Public, Cache, Logs, filesystemMedia) con mkdir(..., 0755, true). La successiva chiamata a createAdditionalFolders() — pensata apposta per portare Cache, Logs e filesystemMedia a 0777, le uniche tre che l'applicazione deve poter scrivere a runtime, a differenza di Config e Public — eseguiva mkdir()/chmod() solo if (!is_dir($path)): condizione sempre falsa, perché le tre cartelle esistevano già per effetto di createProjectStructure(). Il blocco pensato per garantire la scrittura era quindi sempre inattivo, codice morto fin dalla sua introduzione.
L'effetto pratico: subito dopo l'installazione, Cache/Logs/filesystemMedia restavano a 0755, di proprietà dell'utente che ha eseguito sisma install — tipicamente diverso dall'utente sotto cui gira il server web — impedendo a quest'ultimo di scrivere cache, log e media fino a un eventuale intervento correttivo esterno alla procedura di installazione stessa.
createProjectStructure() ora crea solo Config e Public; la creazione di Cache, Logs e filesystemMedia resta di esclusiva competenza di createAdditionalFolders(), che a questo punto le trova sempre assenti ed esegue realmente mkdir(0777, true) + chmod(0777).
File modificati:
Console/Services/Installation/InstallationManager.php: createProjectStructure(), rimosse Cache, Logs, filesystemMedia dall'elenco delle directory createCache, Logs e filesystemMedia risultano ora scrivibili (0777) subito dopo l'installazione, come già previsto — ma mai applicato — dal codice preesistente.
Patch che completa la correzione avviata in 12.0.3. La guardia headers_sent() introdotta in quella release per evitare la cascata di pagine d'errore duplicate in produzione impediva, come effetto collaterale non voluto, anche la visualizzazione della pagina di dettaglio per i warning residui in ambiente di sviluppo: da quando RenderService scarica realmente l'output a fine rendering (12.0.2), all'avvio della shutdown function registrata da registerNonThrowableErrorHandler() (che PHP esegue solo alla terminazione effettiva dello script) gli header risultavano ormai quasi sempre già inviati per qualunque richiesta completata con successo, facendo uscire la guardia prima ancora di controllare l'ambiente. Il risultato era la contraddizione esplicitamente esclusa dal changelog 12.0.3: un warning residuo, anche in sviluppo, non generava più visibleError.
Core/HelperClasses/ErrorHandler::registerNonThrowableErrorHandler() — warning residui non mostravano più la pagina di dettaglio in sviluppoLa causa non era l'ordine dei controlli nella shutdown function, ma il fatto che il contenuto della risposta fosse già stato scaricato per davvero (quindi irrecuperabile) ben prima che la shutdown function avesse la possibilità di intervenire. PHP mantiene invece aperti i buffer di output durante l'esecuzione delle shutdown function, scaricandoli automaticamente solo al termine di tutte — la stessa proprietà su cui si basava implicitamente il comportamento (corretto) precedente alla chiusura esplicita del buffer introdotta in 12.0.2.
registerNonThrowableErrorHandler() apre ora un buffer-involucro (ob_start() grezzo, non tracciato da BufferManager) non appena viene invocato, prima ancora di registrare la shutdown function: essendo aperto fuori dal livello base tracciato da BufferManager, i normali cicli start()/flush() di RenderService — inclusi quelli innescati da rendering completati con successo — vi trasferiscono il contenuto senza mai scaricarlo realmente all'esterno, lasciandolo recuperabile fino alla reale terminazione dello script. Quando la shutdown function deve sostituire integralmente la risposta (sempre in sviluppo; solo per errori realmente fatali in produzione, dopo la consueta verifica headers_sent()), il nuovo metodo BufferManager::discardAll() scarta anche questo involucro — cosa che il precedente clear(), vincolato a non retrocedere sotto il livello base per non toccare buffer esterni al framework, non poteva fare — e resetta il livello base, cosicché il rendering della pagina d'errore che segue venga effettivamente consegnato come unico output della richiesta.
File modificati:
Core/HelperClasses/BufferManager.php: aggiunto il metodo statico discardAll()Core/HelperClasses/ErrorHandler.php: registerNonThrowableErrorHandler() apre il buffer-involucro; sostituito BufferManager::clear() con BufferManager::discardAll() nel ramo di sostituzione della rispostaCore/HelperClasses/Dispatcher::run() — warning di deprecazione residuo su QUERY_STRING assente
Portato alla luce testando la correzione precedente: strlen($this->request->server['QUERY_STRING']) generava un warning di deprecazione (strlen(): Passing null...) ogni volta che la richiesta non aveva query string e la chiave non risultava valorizzata nell'array $_SERVER (caso riscontrato con il server PHP integrato). Il warning, residuo a fine richiesta, veniva finora silenziato dal difetto descritto sopra; una volta corretto quest'ultimo, avrebbe fatto scattare la pagina di dettaglio ad ogni richiesta priva di query string.
File modificati:
Core/HelperClasses/Dispatcher.php: run(), strlen($this->request->server['QUERY_STRING'] ?? '')docs-phpdoc/ — Rigenerazione completa
Rigenerata tramite composer phpdoc per riflettere il nuovo metodo BufferManager::discardAll().
BufferManager::discardAll() è una nuova aggiunta puramente additiva.http_response_code()/Flush e Cascata di Pagine d'Errore DuplicatePatch che corregge due difetti resi visibili dalla chiusura esplicita del buffer introdotta in 12.0.2: impostare il codice di risposta HTTP dopo aver già scaricato l'output al client, e una shutdown function che non distingueva un errore fatale reale da un semplice warning residuo né verificava se una risposta fosse già stata inviata.
Core/Services/RenderService — http_response_code() impostato dopo l'invio dell'output
Da quando BufferManager::flush() chiude realmente tutti i livelli di buffer del framework fino al floor (12.0.2), invocarlo durante il rendering scarica per davvero il contenuto al client, gli header compresi. generateView(), generateData() e generateJson() costruivano però l'oggetto Response — il cui costruttore chiama http_response_code() per impostare il codice desiderato (es. 500 per una pagina d'errore) — solo dopo aver chiamato BufferManager::flush(). A quel punto gli header erano già stati inviati, e PHP generava il warning http_response_code(): Cannot set response code - headers already sent (output started at .../BufferManager.php:55).
Spostata la costruzione di Response prima di BufferManager::flush() in tutti e tre i metodi: il codice di stato viene ora impostato mentre l'output è ancora bufferizzato, prima che gli header vengano effettivamente inviati.
File modificati:
Core/Services/RenderService.php: generateView(), generateData(), generateJson()Core/HelperClasses/ErrorHandler::registerNonThrowableErrorHandler() — cascata di pagine d'errore duplicate
Il warning descritto sopra, in quanto ultimo errore PHP della richiesta, veniva intercettato da error_get_last() nella shutdown function registrata da registerNonThrowableErrorHandler(). Questa si limitava a verificare is_array($error), senza distinguere un errore realmente fatale da un semplice warning né controllare se una risposta fosse già stata inviata per la richiesta corrente: trattava quindi il warning come un crash non catturato e tentava di renderizzare un'ulteriore pagina d'errore completa, la quale — generando a sua volta lo stesso warning — poteva ripetere la cascata una terza volta, questa volta mostrata "nuda" da PHP perché display_errors era già stato riattivato da showErrorInDevelopmentEnvironment() (chiamato nel blocco finally di Public/index.php, eseguito prima della shutdown function).
Aggiunte due guardie alla shutdown function:
headers_sent()), l'errore viene solo loggato, senza tentare di renderizzare un'altra pagina che si limiterebbe ad accodarsi all'output già in volo;callInternalServerErrorAction()) viene ora invocata solo se l'errore appartiene ai livelli realmente fatali (E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR), tramite il nuovo metodo privato isFatalError(); un warning residuo viene loggato ma non genera più una pagina di errore al posto della risposta già prodotta.
In ambiente di sviluppo il comportamento resta volutamente invariato: qualsiasi errore residuo, warning incluso, continua a generare la pagina di dettaglio (nonThrowableError()), scelta progettuale per rendere visibile ogni anomalia durante lo sviluppo.
File modificati:
Core/HelperClasses/ErrorHandler.php: registerNonThrowableErrorHandler(), aggiunto il metodo privato isFatalError()Core/HelperClasses/Dispatcher/ControllerFactory::createController() — rimossa condizione "fast path" ridondante
Il metodo selezionava un "fast path" (new $controllerClassName($this->dataMapper, $this->debugger)) per i costruttori senza parametri, con un solo parametro DataMapper, o con DataMapper seguito da Debugger. Per tutti e tre i casi, il percorso generico resolveConstructorArguments() produce però esattamente lo stesso risultato: PHP ignora silenziosamente gli argomenti posizionali passati in eccesso rispetto ai parametri dichiarati da un costruttore, quindi passare sempre $dataMapper/$debugger a un costruttore che ne dichiara meno (o nessuno) equivaleva già a quanto fa resolveConstructorArguments() valutando i tipi dichiarati uno per uno. La condizione non copriva inoltre alcun caso non già gestito dal percorso generico: era codice morto.
Rimossa la condizione: createController() passa ora sempre attraverso resolveConstructorArguments().
File modificati:
Core/HelperClasses/Dispatcher/ControllerFactory.phpdocs-phpdoc/ — Rigenerazione completa
Rigenerata tramite composer phpdoc. Nessuna modifica di rilievo: le correzioni di questa release non toccano segnature pubbliche né aggiungono classi, l'unico file con differenze (SismaFramework-Core-Services-RenderService.html) riflette esclusivamente lo spostamento dei numeri di riga dovuto al riordino del codice in RenderService.php.
RenderService, ErrorHandler e ControllerFactory restano invariate; tutte le correzioni intervengono solo sulla logica interna.
Patch che rende esplicita la chiusura del buffer di output aperto da RenderService durante il rendering, invece di fare affidamento sullo scaricamento implicito a fine script. La correzione ha permesso di eliminare un workaround diventato necessario nella suite di test e ha reso visibili due difetti preesistenti che il workaround mascherava.
Core/Services/RenderService — Buffer di rendering non chiuso esplicitamente
assemblesComponents() (usato da generateView() e generateData()) e generateJson() aprono un livello di buffer tramite BufferManager::start() ma non lo richiudevano mai esplicitamente: il contenuto renderizzato restava bufferizzato fino alla chiusura naturale dello script PHP, che scarica automaticamente i buffer residui a fine richiesta. Questo comportamento implicito funziona in un normale ciclo richiesta/risposta (un processo per richiesta), ma lascia il buffer indefinitamente "in sospeso" in qualunque contesto che riutilizzi lo stesso processo per più cicli logici — come la suite di test, dove PHPUnit esegue centinaia di test nello stesso processo.
Aggiunta una chiamata esplicita a BufferManager::flush() al termine di generateView(), generateData() e generateJson(), subito prima di restituire la Response: il buffer aperto da ciascuna chiamata viene ora sempre chiuso nello stesso punto logico in cui viene aperto, senza fare affidamento sullo scaricamento implicito di fine script. Valutata anche l'aggiunta della stessa chiamata in Dispatcher::run(): verificato che è superflua, perché BufferManager::start() viene invocato solo da RenderService, che ora si chiude sempre da solo.
File modificati:
Core/Services/RenderService.php: generateView(), generateData() e generateJson() chiudono il proprio buffer con BufferManager::flush() prima di ritornare
Con BufferManager::flush() ora invocato esplicitamente da RenderService, la suite di test ha smesso di richiedere il workaround manuale (\ob_end_clean() a inizio test) precedentemente necessario per compensare il buffer di rendering mai chiuso. Rimosse le chiamate manuali a \ob_end_clean() in DispatcherTest, RenderTest e RenderServiceTest, e unificate le chiamate multiple a expectOutputRegex() per singolo test (non supportate da PHPUnit oltre la prima) in un'unica regex con lookahead.
La correzione ha inoltre reso visibili due difetti preesistenti, prima mascherati dal workaround:
TestsApplication/Views/**/*.php) includevano i partial comuni (baseHead.php, menu.php, footer.php, header.php) con require_once: nel processo unico condiviso da PHPUnit, solo il primo render dell'intera suite li includeva realmente, lasciando <head> vuoto in tutti i render successivi. Cambiato in require.DispatcherTest::testNotifyPath verificava un titolo di pagina errato (sample - index invece di sample - notify, azione effettivamente instradata).File modificati:
Tests/Core/HelperClasses/DispatcherTest.php, Tests/Core/HelperClasses/RenderTest.php, Tests/Core/Services/RenderServiceTest.php: rimossi gli \ob_end_clean() manuali, unificate le expectOutputRegex() multiple, corretta l'asserzione di testNotifyPathTestsApplication/Views/controllerWithSlug/index.php, TestsApplication/Views/other/*.php, TestsApplication/Views/sample/*.php: require_once → require per l'inclusione dei partial comuniRenderService resta invariata; la correzione interviene solo sulla logica interna.sisma upgrade (11.x → 12.0.0) e Miglioramenti al Sito di Autopromozionesisma upgrade (11.x → 12.0.0)
La strategy Upgrade11to12Strategy copriva solo 2 delle 3 breaking change della 12.0.0 (rinomina SelfReferencedModel e riordino parametri di setFulltextIndexColumn), lasciando silenziosa la rimozione dei metodi deprecati e delle classi LogException/NoLogException.
File aggiunti:
Console/Services/Upgrade/Transformers/ExceptionBaseClassTransformer.php: Riscrive extends LogException/extends NoLogException in extends BaseException (aggiungendo implements ShouldBeLoggedException per LogException) e aggiorna gli use statement corrispondenti; segnala per revisione manuale i casi non gestibili automaticamente (riferimento senza extends, use mancante)Console/Services/Upgrade/Transformers/DeprecatedMethodUsageTransformer.php: Rileva le chiamate ai metodi rimossi da DependentModel/SelfDependentModel (countEntityCollectionByEntity(), getEntityCollectionByEntity(), deleteEntityCollectionByEntity(), e le controparti ...ByParentAndEntity()) e le segnala per la migrazione manuale ai metodi magici, poiché il nome del metodo di destinazione dipende dal nome della proprietà usato a runtime e non può essere riscritto in modo sicuro con una trasformazione testualeFile modificati:
Console/Services/Upgrade/Strategies/Upgrade11to12Strategy.php: Aggiunti i due nuovi transformer e ampliato getBreakingChanges() con la rimozione dei metodi deprecati, la rimozione di LogException/NoLogException e la rimozione delle costanti inutilizzate da Config/config.phpConsole/Services/Upgrade/UpgradeManager.php: I warning raccolti dai transformer vengono ora inclusi nel report anche per i file che non hanno subito modifiche automatiche di contenuto (prima erano scartati se changesCount restava a 0, es. per un transformer di sola rilevazione)robots.txt e sitemap.xml non raggiungibili (404) sul sito di autopromozione
Le richieste a /robots.txt e /sitemap.xml restituivano la pagina di errore generica del sito (Errore seguito dal path grezzo) invece del contenuto atteso. I due file erano posizionati nella root del modulo Sample/, ma ResourceHandler::handleResourceFile() risolve i file statici solo in tre percorsi: la root di progetto, gli assets strutturali del framework e la cartella Assets/ del modulo applicativo (Sample/Assets/) — mai la root del modulo stesso, che è riservata all'albero di classi/viste (Controllers/, Models/, Views/, ecc.). Non trovando il file in nessuno dei tre percorsi, il dispatcher lanciava una PageNotFoundException con il path richiesto come messaggio, gestita da SampleController::error() come una pagina di errore qualsiasi.
File spostati:
Sample/robots.txt → Sample/Assets/robots.txtSample/sitemap.xml → Sample/Assets/sitemap.xmlConfig/config.php — La root del sito (/) serviva la pagina degli esempi invece della homepage
DEFAULT_PATH era impostato su sample: RouteResolver::parsePath() lo usa come controller quando l'URL è vuoto, quindi ogni richiesta a / risolveva a SampleController::index() (la lista di articoli demo) invece che a HomeController::index() (la landing page del framework). HomeController::welcome(), il cui docblock dichiara URL: /, era di fatto irraggiungibile da quel path — mentre sitemap.xml e robots.txt indicavano / come homepage con priorità massima.
File modificati:
Config/config.php: DEFAULT_PATH da "sample" a "home" — la root ora risolve direttamente a HomeController::index()Core/HelperClasses/Dispatcher/ControllerFactory.php — Istanziazione errata dei controller con parametri di costruttore aggiuntivi dopo DataMapper
createController() decideva se usare la "fast path" di istanziazione (new $controllerClassName($this->dataMapper, $this->debugger)) controllando solo che il primo parametro del costruttore fosse di tipo DataMapper, senza verificare che il secondo fosse effettivamente Debugger. Per un controller con firma __construct(DataMapper $dataMapper, XxxService $xxxService) — come Sample/Controllers/HomeController.php, DocsController.php e SampleController.php, che accettano FrameworkInfoService come secondo parametro — veniva comunque intrapresa la fast path, passando un'istanza di Debugger al posto del servizio atteso: il dispatcher reale falliva con un TypeError all'istanziazione del controller.
Il controllo ora enumera esplicitamente i soli casi ammessi per la fast path: costruttore senza parametri, un solo parametro di tipo DataMapper, oppure esattamente due parametri rispettivamente DataMapper e Debugger. Qualsiasi altra combinazione (inclusi eventuali costruttori con tre o più parametri) passa sempre dalla risoluzione generica resolveConstructorArguments(), che istanzia ogni parametro in base al proprio tipo — più robusta anche di un controllo basato solo su "il secondo parametro è Debugger?", che avrebbe comunque richiamato la fast path (con soli due argomenti posizionali) anche in presenza di un eventuale terzo parametro obbligatorio.
File modificati:
Core/HelperClasses/Dispatcher/ControllerFactory.phpSample/Controllers/SampleController::protected() — Metodi inesistenti su Authentication
L'azione chiamava $auth->isLogged(), $auth->getAuthenticatedUser() e $auth->getUserIdentifier() su Security\HttpClasses\Authentication: nessuno di questi tre metodi esiste, né su quella classe né sulla base BaseAuthentication (che espone solo getAuthenticableInterface(), utilizzabile esclusivamente nella stessa richiesta in cui checkAuthenticable() ha già validato le credenziali). Qualunque richiesta a /sample/protected falliva con un Error: Call to undefined method. Mancava inoltre la view sample/protected.php, mai creata.
Come documentato in docs/security.md, Authentication si occupa solo della validazione delle credenziali in fase di login; la persistenza dello stato di autenticazione tra richieste va gestita tramite Session. Riscritta l'azione per verificare Session::hasItem('userId'), coerente con l'esempio di login già presente nella documentazione. Il modulo demo non include un flusso di login (nessuna entity implementa AuthenticableInterface), quindi l'azione dimostra il pattern di guardia ma reindirizza sempre alla pagina di errore in assenza di sessione.
File aggiunti:
Sample/Views/sample/protected.phpFile modificati:
Sample/Controllers/SampleController.php: protected() non richiede più Authentication come parametro, usa Session::hasItem()/Session::getItem()Core/HelperClasses/BufferManager — clear()/flush() non gestivano output buffer annidati
clear() e flush() operavano solo sul livello di output buffer più interno (ob_clean()/ob_flush() agiscono su un singolo livello), mentre diverse view che si appoggiano a siteLayout.php aprono un proprio buffer annidato (ob_start() per catturare $content prima di includere il layout condiviso) dentro quello già aperto da RenderService::assemblesComponents(). Se un'eccezione (ogni BaseException, incluse tutte le eccezioni HTTP del framework, chiama BufferManager::clear() nel proprio costruttore) veniva sollevata mentre il buffer della view era ancora aperto, clear() ripuliva solo quel livello interno, lasciando aperto e non azzerato il livello esterno del framework — una correttezza che si è retta finora solo sul fatto che quel livello esterno risultava sempre vuoto in pratica, non su una garanzia del codice.
clear()/flush() ora svuotano in loop tutti i livelli di buffer aperti dal framework, tramite ob_end_clean()/ob_end_flush(). Per evitare di richiudere buffer non di proprietà del framework (es. quello di PHPUnit durante i test, o un eventuale buffer aperto dal server prima dell'avvio della richiesta), la classe memorizza ora un livello di base ($baseLevel), rilevato al primo utilizzo, sotto il quale i due metodi non scendono mai. Una prima versione che svuotava incondizionatamente fino al livello 0 assoluto è stata scartata perché rompeva l'isolamento dei buffer di PHPUnit (57 test segnalati come "risky" nella suite completa).
File modificati:
Core/HelperClasses/BufferManager.phpdocs-phpdoc/ — Rigenerazione completa
Rigenerata tramite composer phpdoc per includere le nuove classi introdotte in questa release (ExceptionBaseClassTransformer, DeprecatedMethodUsageTransformer) e riflettere le modifiche a Upgrade11to12Strategy e UpgradeManager. Sample/, Config/ e Tests/ restano esclusi dalla generazione (phpdoc.xml), quindi le modifiche al sito di autopromozione e alla configurazione di questa release non compaiono in questa documentazione API.
ob_start()/ob_get_clean() dalle view (commonParts/siteLayout.php sostituito da header/footer)
Le view basate sul layout condiviso (home/*, docs/*, quasi tutte le sample/*) catturavano il proprio contenuto con ob_start()/ob_get_clean() in una variabile $content, poi interpolata a metà di siteLayout.php tra navbar e footer — un buffer annidato dentro quello già aperto da RenderService::assemblesComponents(). Non è il pattern documentato dal framework: docs/views.md indica esplicitamente di dividere il layout in header.php/footer.php e usare require sequenziali, proprio per evitare qualunque trucco di output buffering nelle view. Le view più vecchie del sito (error.php, notify.php) seguivano già correttamente questo pattern.
Sample/Views/commonParts/siteLayout.php è stato sostituito da siteLayoutHeader.php (tutto fino alla chiusura della navbar) e siteLayoutFooter.php (dal footer alla chiusura di </html>). Le 12 view interessate ora fanno require del primo prima del contenuto e del secondo dopo, senza alcun output buffering proprio. Verificato con rendering reale (PHP built-in server) di tutte le pagine coinvolte, oltre alla suite PHPUnit completa.
File aggiunti:
Sample/Views/commonParts/siteLayoutHeader.php, Sample/Views/commonParts/siteLayoutFooter.phpFile rimossi:
Sample/Views/commonParts/siteLayout.phpFile modificati:
Sample/Views/home/index.php, home/privacy.php, home/cookies.php, docs/index.php, docs/changelog.php, docs/viewer.php, sample/index.php, sample/articlesByAuthor.php, sample/filterByStatus.php, sample/search.php, sample/showArticle.php, sample/protected.phpSample/README.md, Sample/SITE_INFO.md: riferimenti a siteLayout.php aggiornati
La versione corrente del framework e la data di rilascio non erano visibili in nessuna pagina del sito di autopromozione (Sample), se non nel CHANGELOG.md. Aggiunto un badge versione in navbar (linkato a /docs/changelog) e una riga "Versione X.Y.Z — rilasciata il ..." nel footer; corretto anche il campo softwareVersion nel JSON-LD della homepage, rimasto hardcoded a 10.0.3 per diverse major.
Le informazioni vengono ricavate automaticamente da composer.json (versione) e dal primo blocco datato del CHANGELOG.md (data di rilascio corrispondente), tramite un nuovo Service iniettato nei controller — coerente con il pattern di dependency injection già usato per DataMapper/Debugger — evitando così di doverle allineare manualmente ad ogni rilascio.
File aggiunti:
Sample/Services/FrameworkInfoService.php: legge la versione da composer.json e la data di rilascio corrispondente dal CHANGELOG.mdFile modificati:
Sample/Controllers/HomeController.php, Sample/Controllers/DocsController.php, Sample/Controllers/SampleController.php: iniettato FrameworkInfoService e valorizzate le var frameworkVersion/frameworkReleaseDateSample/Views/commonParts/siteLayout.php: badge versione in navbar, riga versione/data nel footer, softwareVersion dinamico nel JSON-LDRimossi gli script usati per diagnosticare il bug del parser Markdown risolto in 11.8.1, mai ripuliti dopo il fix, incluso un metodo di controller raggiungibile pubblicamente via URL.
File rimossi:
Sample/test_parser.php, Sample/test_regex.php, Sample/debug_markdown.phpFile modificati:
Sample/Controllers/DocsController.php: rimosso il metodo debugRegex() (azione pubblica non referenziata da alcuna vista o link)Sample/module.json — Allineamento versione
framework_version e requires.sismaframework erano rimasti fermi a 11.0.0; allineati a 12.0.1.
File modificati:
Sample/module.jsonSample/Assets/sitemap.xml — Allineamento con le pagine reali del sito
La sitemap indicizzava solo 2 delle 31 pagine di documentazione (getting-started, api-reference), non includeva /docs/changelog, ed elencava tutte le pagine con <lastmod> fermo al 2025-01-15. Riscritta per includere l'intero albero di docs/index.md, raggruppato per sezione con priorità decrescente, e aggiornate le date.
File modificati:
Sample/Assets/sitemap.xmlSample); non toccano alcuna API pubblica del framework.SelfReferencedModel e Riordinamento Parametri QuerySelfReferencedModel → SelfDependentModel
La classe SelfReferencedModel è stata rinominata in SelfDependentModel per allineare la nomenclatura del layer model al concetto di dipendenza (anziché referenziazione). Nel layer model la chiave classificatoria è la presenza di chiavi esterne nell'entità gestita (dipendenza), non il fatto di essere referenziata da altre entità (referenziazione, concetto proprio del layer entity). L'autoreferenziazione è bidirezionale, quindi un'entità autoreferenziata è anche autodipendente; il nome SelfDependentModel riflette correttamente la prospettiva del layer model.
File modificati:
Orm/ExtendedClasses/SelfDependentModel.php: Rinominato da SelfReferencedModel.php; classe rinominata da SelfReferencedModel a SelfDependentModelConsole/Enumerations/ModelType.php: Case selfReferencedModel → selfDependentModel, valore "SelfReferencedModel" → "SelfDependentModel"Console/Commands/ScaffoldCommand.php: Aggiornati i riferimenti stringaSample/Models/SampleSelfReferencedEntityModel.php: use e extends aggiornatiTests/Orm/ExtendedClasses/SelfDependentModelTest.php: Rinominato da SelfReferencedModelTest.php; aggiornati use e nome classe
Migrazione: Sostituire ogni occorrenza di SelfReferencedModel con SelfDependentModel nei use statement e nelle dichiarazioni extends dei propri model.
Query::setFulltextIndexColumn() — Riordinamento parametri
I parametri del metodo setFulltextIndexColumn della classe Query sono stati riordinati per garantire coerenza con la firma degli altri metodi della classe.
File modificati:
Orm/HelperClasses/Query.php: Riordinati i parametri di setFulltextIndexColumnTests/Orm/HelperClasses/QueryTest.php: Aggiornate le chiamate di test
Migrazione: Verificare e aggiornare l'ordine degli argomenti in ogni chiamata a setFulltextIndexColumn.
Come pianificato, i metodi e le classi deprecati con rimozione prevista in questa versione sono stati eliminati.
DependentModel — rimossi i metodi deprecati dalla v10.1.0:
countEntityCollectionByEntity(), getEntityCollectionByEntity(), deleteEntityCollectionByEntity() → usare i metodi magici countBy{PropertyName}(), getBy{PropertyName}(), deleteBy{PropertyName}()
SelfDependentModel — rimossi i metodi deprecati dalla v10.1.0:
countEntityCollectionByParentAndEntity(), getEntityCollectionByParentAndEntity(), deleteEntityCollectionByParentAndEntity() → usare i metodi magici countByParentAnd{PropertyName}(), getByParentAnd{PropertyName}(), deleteByParentAnd{PropertyName}()
Security/ExtendedClasses/LogException e Security/ExtendedClasses/NoLogException — rimosse interamente, deprecate dalla v11.0.0. AccessDeniedException ora estende direttamente BaseException implementando ShouldBeLoggedException.
File modificati:
Orm/ExtendedClasses/DependentModel.php, Orm/ExtendedClasses/SelfDependentModel.php: rimossi i metodi deprecatiOrm/ExtendedClasses/ReferencedEntity.php: aggiornato l'uso interno dei metodi deprecati con chiamate dinamiche ai metodi magici corrispondentiCore/Exceptions/AccessDeniedException.php: estende ora BaseException implementando ShouldBeLoggedException direttamenteSample/Models/SampleDependentEntityModel.php: rimosso l'esempio basato sui metodi deprecatiTests/Orm/ExtendedClasses/DependentModelTest.php, Tests/Orm/ExtendedClasses/SelfDependentModelTest.php: test aggiornati per usare i metodi magiciFile rimossi:
Security/ExtendedClasses/LogException.phpSecurity/ExtendedClasses/NoLogException.php
Migrazione: Sostituire ogni chiamata ai metodi rimossi con l'equivalente metodo magico; sostituire extends LogException/extends NoLogException con extends BaseException (implementando ShouldBeLoggedException se la classe deve essere loggata).
sisma upgrade
Il sistema di upgrade automatico dei moduli ora copre anche la migrazione dalla versione 11 alla 12. Le due breaking change di questa versione vengono gestite tramite due nuovi transformer, integrati nella nuova strategy Upgrade11to12Strategy.
Transformer ClassRenameTransformer (confidence: 95%):
use statement, dichiarazioni extends e riferimenti a enum caseSelfReferencedModel → SelfDependentModel e selfReferencedModel → selfDependentModel
Transformer FulltextIndexColumnTransformer (confidence: 70%):
setFulltextIndexColumn() alla nuova firmarequiresManualReview con warningFile aggiunti:
Console/Services/Upgrade/Transformers/ClassRenameTransformer.php: Nuovo transformer generico per rinominare identificatoriConsole/Services/Upgrade/Transformers/FulltextIndexColumnTransformer.php: Nuovo transformer per il riordino parametri di setFulltextIndexColumn()Console/Services/Upgrade/Strategies/Upgrade11to12Strategy.php: Nuova strategy per l'upgrade 11.x → 12.0.0Tests/Console/Services/Upgrade/Strategies/Upgrade11to12StrategyTest.php: Test della nuova strategyTests/Console/Services/Upgrade/Transformers/ClassRenameTransformerTest.php: Test del ClassRenameTransformerTests/Console/Services/Upgrade/Transformers/FulltextIndexColumnTransformerTest.php: Test del FulltextIndexColumnTransformerFile modificati:
Console/Services/Upgrade/UpgradeManager.php: Aggiunta Upgrade11to12Strategy all'array delle strategy in selectStrategy()Console/Traits/InteractiveInputTrait — Iniezione dello stream di input per i test
Il trait espone ora il metodo pubblico setInputStream(), che permette di sostituire lo stream php://stdin usato da ask(), askConfirmation() e askSecret() con uno stream arbitrario (es. php://memory), rendendo testabili i comandi interattivi senza dover simulare un vero input da terminale. Lo stream, se non iniettato esplicitamente, viene aperto una sola volta e riutilizzato tra le chiamate successive (in precedenza veniva aperto e richiuso ad ogni singola richiesta).
File modificati:
Console/Traits/InteractiveInputTrait.php: aggiunti i metodi setInputStream() e getInputStream(); lo stream è ora conservato nella proprietà $inputStream invece di essere aperto/chiuso ad ogni chiamataTests/Console/Traits/InteractiveInputTraitTest.php: nuovi test basati su stream di input iniettatiTests/Console/Commands/InstallationCommandTest.php: nuovi test del flusso di configurazione interattiva del database basati su stream di input iniettatiConfig/config.php e Core/HelperClasses/Config.php — Rimozione costanti non utilizzate
Rimosse le costanti dichiarate in Config/config.php (e le relative proprietà esposte da Core/HelperClasses/Config.php) non più referenziate da alcuna classe del framework: ADAPTERS, ADAPTER_NAMESPACE, ADAPTER_PATH, CONFIGURATION_PASSWORD, CORE, CORE_NAMESPACE, CORE_PATH, DEFAULT_CONTROLLER, DEFAULT_CONTROLLER_NAMESPACE, DEFAULT_CONTROLLER_PATH, DEFAULT_META_URL, MODEL_PATH, ORM, ORM_NAMESPACE, ORM_PATH, PUBLIC_PATH, STRUCTURAL_RESOURCES_PATH, THIS_DIRECTORY.
Rimosse anche LOG_WARNING_ROW e LOG_DANGER_ROW: non erano lette da alcuna classe del framework (che usa invece LOG_VERBOSE_ACTIVE, LOG_DEVELOPMENT_MAX_ROW e LOG_PRODUCTION_MAX_ROW per la rotazione dei log), ma solo da moduli applicativi esterni per colorare un indicatore nella dashboard di back-end. La loro dichiarazione va spostata nel file di configurazione del modulo consumatore, secondo il pattern già seguito da costanti equivalenti (es. soglie di warning/danger per la dimensione dei media).
File modificati:
Config/config.php: rimosse le costanti sopra elencateCore/HelperClasses/Config.php: rimosse le proprietà readonly corrispondentidocs/configuration-reference.md, docs/controllers.md: aggiornati i riferimenti alle costanti rimosse
Migrazione: chi facesse riferimento diretto a una di queste costanti tramite \Config\NOME_COSTANTE in un modulo applicativo deve dichiararla nel file di configurazione del proprio modulo.
Questa minor release permette di configurare le credenziali del database e la passphrase di cifratura tramite variabili d'ambiente, evitando di doverle scrivere come valori letterali in Config/configFramework.php — file che, a differenza di Config/config.php, viene generato durante l'installazione e tipicamente committato nel progetto applicativo. L'installer rileva automaticamente quando queste variabili sono già presenti nell'ambiente e salta la richiesta interattiva/CLI corrispondente.
Config/config.php — Costanti database e cifratura risolte tramite getenv()
Le costanti DATABASE_HOST, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_PORT ed ENCRYPTION_PASSPHRASE non sono più dichiarate con const ma con define(__NAMESPACE__ . '\NOME_COSTANTE', getenv('NOME_COSTANTE') ?: ""). Questo era necessario perché PHP non ammette chiamate a funzione (getenv()) all'interno di un'espressione costante dichiarata con const; define() accetta invece un'espressione valutata a runtime. Il namespace va qualificato esplicitamente (__NAMESPACE__ . '\...') perché, a differenza di const, define() non eredita automaticamente il namespace del file.
Se la variabile d'ambiente non è impostata, il fallback resta la stringa vuota "", identico al valore di default precedente: il comportamento per chi non adotta variabili d'ambiente è invariato.
File modificati:
Config/config.php: le sei costanti elencate sopra convertite da const a define() con fallback a getenv()Console/Commands/InstallationCommand::collectDatabaseConfiguration() — Skip automatico con variabili d'ambiente già presenti
Aggiunto il metodo privato hasDatabaseConfigFromEnvironment(), che verifica se almeno una tra DATABASE_HOST, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_PORT è già impostata nell'ambiente del processo. Il controllo viene eseguito dopo quello sulle opzioni CLI (--db-host e affini, che restano prioritarie se esplicitamente fornite) e prima del prompt interattivo: se l'ambiente fornisce già la configurazione, l'installer stampa un messaggio informativo e prosegue senza chiedere nulla, evitando di scrivere un fallback letterale — e quindi un potenziale segreto — in Config/configFramework.php.
Aggiornato anche il testo di --help del comando per documentare il nuovo comportamento.
File modificati:
Console/Commands/InstallationCommand.php: aggiunto hasDatabaseConfigFromEnvironment(); collectDatabaseConfiguration() lo invoca prima del prompt interattivo; aggiornato il testo di configure().env.example — Documentazione delle variabili d'ambiente consultate dal framework
Aggiunto un file .env.example nella root del framework che elenca le sei variabili d'ambiente lette via getenv() in Config/config.php. Il file è puramente documentale: il framework non effettua alcun parsing di file .env (nessuna nuova dipendenza, nessun loader interno) — le variabili devono essere rese disponibili come variabili d'ambiente del processo PHP da chi gestisce il deployment (Docker env_file, direttive del web server, systemd EnvironmentFile, export manuale, ecc.).
File creati:
.env.exampleConsole/Services/Installation/InstallationManager::updateConfigFile() — Supporto al nuovo pattern define()+getenv()
La sostituzione dei valori raccolti da CLI/prompt interattivo in Config/configFramework.php riconosceva solo il pattern const NOME = "valore";. Aggiunto un secondo pattern che riconosce define(__NAMESPACE__ . '\NOME', getenv('NOME') ?: "valore") e aggiorna soltanto il valore di fallback dopo ?:, lasciando intatta la chiamata a getenv() — così l'ambiente continua ad avere priorità anche su un valore scritto in fase di installazione.
File modificati:
Console/Services/Installation/InstallationManager.php: updateConfigFile() applica entrambi i pattern (const e define()+getenv()) per ciascuna chiave di configurazioneConfig\DATABASE_HOST, ecc.) restano identici per chi le consulta; cambia solo il meccanismo interno di dichiarazione (define() invece di const)."" di prima — installazioni standalone esistenti non notano differenze.
Patch che corregge il parser Markdown del sito di autopromozione/documentazione (Sample/Controllers/DocsController.php): i fenced code block annidati in una blockquote venivano interpretati in modo scorretto, corrompendo il rendering di tutto il contenuto successivo del documento. Corretto anche un fence orfano nel file docs/advanced-orm.md.
Sample/Controllers/DocsController::parseMarkdown() — Code block annidati in blockquote
La regex che estrae i fenced code block (sequenza di tre backtick) cercava i marcatori ovunque nel testo, senza considerare un eventuale prefisso > di blockquote. Quando un code block era annidato in una blockquote (es. una riga > seguita dai tre backtick e dal nome del linguaggio), il marcatore di chiusura — anch'esso prefissato da > — non veniva riconosciuto come tale: la regex continuava ad espandersi non-greedy fino ai successivi tre backtick "nudi" (non prefissati da > ) nel documento, inghiottendo tutto il contenuto intermedio (inclusi altri code block, header e liste) in un unico blocco corrotto con lingua errata.
Aggiunto un nuovo step, eseguito prima dell'estrazione dei code block "normali" e dello step delle blockquote, che riconosce specificamente i fenced code block prefissati da > su ogni riga (apertura, contenuto e chiusura), rimuove il prefisso dal contenuto e li converte in <pre><code> esattamente come gli altri code block. Le righe di blockquote senza code block annidato continuano a essere gestite dallo step esistente.
File modificati:
Sample/Controllers/DocsController.php: aggiunto step di estrazione per i code block annidati in blockquote in parseMarkdown()docs/advanced-orm.md — Fence di chiusura orfanoUn marcatore di chiusura (tre backtick) senza apertura corrispondente era stato lasciato per errore dopo una lista (sezione "Best Practices" del capitolo sul lazy loading). Da quel punto in poi, l'alternanza apertura/chiusura dei fence successivi nel documento risultava sfasata, producendo blocchi di codice corrotti che inghiottivano header e sezioni successive.
File modificati:
docs/advanced-orm.md: rimosso il fence orfanoSample) e un file Markdown di documentazione; non tocca alcuna API pubblica del framework.addRequest() in BaseForm e Standardizzazione Formattazione
Questa minor release estende la flessibilità del metodo addRequest() della classe BaseForm permettendo il controllo esplicito sulle sovrascritture di valori nella request. Inoltre, standardizza la formattazione del codice con miglior indentazione e trailing comma secondo le best practice PHP moderne.
Core/BaseClasses/BaseForm::addRequest() — Controllo esplicito sulle sovrascritture
Il metodo addRequest() è stato esteso con due miglioramenti:
Ampliamento tipi di $value:
string|array $valuestring|int|float|bool|array|null $valueConsente di iniettare nella request dati di diversi tipi primitivi, non solo stringhe e array.
Aggiunta parametro $override con controllo sulle sovrascritture:
protected function addRequest(string $propertyName, string|array $value): self — sovrascrive sempre il valoreprotected function addRequest(string $propertyName, string|int|float|bool|array|null $value, bool $override = true): self
Il parametro $override = true (default) mantiene il comportamento precedente: sovrascrive sempre. Passando false, il metodo scrive il valore solo se la proprietà non esiste ancora in request->input.
Caso d'uso:
// Inietta un valore di default che non sovrascrive l'input dell'utente
$this->addRequest('email', 'default@example.com', override: false);
File modificati:
Core/BaseClasses/BaseForm.php: Firma di addRequest() aggiornata; logica di controllo sulla sovrascrittura implementataStandardizzate le convenzioni di formattazione secondo PSR-12:
Costruttore di BaseForm:
__construct(Metodi con parametri multipli:
validate() e resolveEntity()Vantaggi della trailing comma:
File modificati:
Core/BaseClasses/BaseForm.php: Formattazione costruttore e indentazione parametri standardizzateTests/Core/BaseClasses/BaseFormTest — Copertura nuovo parametro $override
Aggiunti tre test per verificare il comportamento del nuovo parametro $override in addRequest():
testAddRequestWithOverrideTrueShouldOverwriteExistingValue(): Verifica che con override = true (default), un valore esistente viene sovrascritto dalla logica di injectRequest()testAddRequestWithOverrideFalseShouldNotOverwriteExistingValue(): Verifica che con override = false, un valore preesistente in request->input non viene sovrascrittotestAddRequestWithOverrideFalseShouldInjectMissingValue(): Verifica che con override = false, un valore mancante viene comunque iniettatoFile creati:
TestsApplication/Forms/SimpleEntityWithAddRequestOverrideFalseForm.php: Form di test che utilizza addRequest(..., override: false)File modificati:
Tests/Core/BaseClasses/BaseFormTest.php: Aggiunti tre nuovi test$override = true mantiene il comportamento precedente per tutto il codice esistentefalse per ottenere il comportamento conservativo--module e Discovery dei Moduli Non Configurati
Introduce l'opzione --module=NomeModulo per selezionare esplicitamente quale modulo deve gestire un comando quando più moduli registrano lo stesso nome. Aggiunge contestualmente la discovery automatica dei moduli fisicamente presenti su filesystem ma non ancora dichiarati in MODULE_FOLDERS, risolvendo il problema di bootstrap circolare per cui un modulo non poteva registrarsi tramite il proprio comando di installazione perché non ancora configurato.
Console/HelperClasses/CommandDispatcher — Opzione --module per selezione esplicita del modulo
Aggiunta l'opzione globale opzionale --module=NomeModulo al dispatcher dei comandi console. Quando specificata, il dispatcher filtra la lista dei comandi compatibili e ne esegue solo uno appartenente al modulo indicato; se nessun comando di quel modulo è compatibile, viene lanciata RuntimeException come per un comando sconosciuto. Senza l'opzione il comportamento rimane invariato (primo match vince, nell'ordine di priorità della discovery).
Per supportare il filtro, ogni comando scoperto viene ora associato al proprio modulo di appartenenza tramite un array parallelo $commandModules[]. La firma di discoverFromDirectory() include il parametro string $module e addCommandStrategy() accetta un secondo parametro opzionale string $module = '' per retrocompatibilità con i chiamanti esistenti.
File modificati:
Console/HelperClasses/CommandDispatcher.php: aggiunto $commandModules[]; run() chiama extractModuleOption() prima del loop; discoverFromDirectory() riceve e salva il modulo; addCommandStrategy() accetta module opzionaleConsole/HelperClasses/CommandDispatcher — Discovery automatica dei moduli non configurati
Il metodo privato discoverUnconfiguredModules() esegue un glob su {rootPath}/*/Console/Commands/ e restituisce le cartelle modulo che hanno quella struttura ma non sono ancora presenti in MODULE_FOLDERS. Questi moduli vengono scansionati dopo quelli configurati e prima del framework, con priorità inferiore rispetto ai moduli dichiarati nella configurazione. La combinazione con --module= permette di eseguire il comando di installazione di un modulo anche prima che sia stato aggiunto a MODULE_FOLDERS.
File modificati:
Console/HelperClasses/CommandDispatcher.php: aggiunto discoverUnconfiguredModules(); discoverCommands() lo invoca tra il loop su moduleFolders e la discovery del frameworkTests/Console/HelperClasses/CommandDispatcherTest — Copertura opzione --module e moduli non configuratitestAddCommandStrategyWithModuleRunsNormally: verifica che addCommandStrategy() con parametro modulo non alteri il comportamento di basetestModuleFilterRunsOnlyMatchingModuleCommand: due comandi compatibili da moduli diversi — con --module=ModuleA solo il primo viene eseguitotestModuleFilterSkipsAllCommandsWhenModuleNotFound: --module=NonExistent → RuntimeException("Unknown command")testModuleFilterWithoutModuleOptionIgnoresModuleOwnership: senza --module la logica "primo match vince" rimane invariatatestDiscoveryFindsCommandsInUnconfiguredModuleFolders: crea su filesystem un modulo assente da MODULE_FOLDERS e verifica che il suo comando sia comunque scoperto ed eseguitotestModuleFilterSelectsUnconfiguredModuleOverConfiguredOne: con due moduli che espongono lo stesso comando, --module=UnconfiguredModule esegue il comando del modulo non configurato ignorando quello configuratoFile modificati:
Tests/Console/HelperClasses/CommandDispatcherTest.php: aggiunti sei testaddCommandStrategy() aggiunge il parametro module con default '' — tutti i chiamanti esistenti compilano senza modifiche. Senza --module il comportamento di run() è identico a prima. I comandi che non usano --module non ricevono l'opzione in modo diverso rispetto alle altre opzioni (finisce in $options['module'] come qualsiasi altra opzione --key=value).
Patch che corregge due bug nel comando di installazione CLI: askSecret() emetteva un errore stty su ambienti senza TTY reale (IDE, pipe, container), e updateConfigFile() troncava i valori numerici come la porta del database a causa di un'ambiguità nelle backreference PCRE della stringa di sostituzione.
Console/Traits/InteractiveInputTrait — Errore stty su stdin non-TTY
Il metodo askSecret() chiamava system('stty -echo') condizionato solo al check PHP_OS === 'WIN', che non copre i casi in cui stdin non è un terminale reale anche su Linux/macOS (IDE come VS Code o PhpStorm, esecuzione via pipe, container Docker, ambienti CI). In questi contesti stty stampava l'errore stty: 'standard input': Inappropriate ioctl for device subito dopo il prompt della password.
Il check è stato sostituito con stream_isatty($handle), che verifica correttamente se il file descriptor è collegato a un TTY reale indipendentemente dal sistema operativo. Se stdin non è un TTY, la password viene letta senza tentare di disabilitare l'echo.
File modificati:
Console/Traits/InteractiveInputTrait.php: askSecret() — rimosso il check PHP_OS === 'WIN', sostituito con stream_isatty($handle)Console/Services/Installation/InstallationManager — Troncamento valori numerici in updateConfigFile()
Il metodo updateConfigFile() costruiva la stringa di sostituzione per preg_replace() interpolando il valore direttamente in una stringa PHP: "$1$2{$value}$2". Quando $value iniziava con una cifra (es. la porta 3306), l'interpolazione produceva la stringa $1$23306$2, che PCRE interpretava come $1 + $23 (backreference al gruppo 23, inesistente → stringa vuota, consumando la prima cifra) + 306 (letterale) + $2 (la virgoletta). Il risultato nel file di configurazione era DATABASE_PORT = 306" — valore troncato e virgoletta di chiusura mancante.
Il metodo è stato riscritto usando preg_replace_callback(): il valore di sostituzione viene concatenato direttamente nella closure PHP, senza mai passare per il parser delle backreference PCRE. Questo risolve anche il caso analogo di password contenenti $ o \.
File modificati:
Console/Services/Installation/InstallationManager.php: updateConfigFile() — preg_replace() sostituito con preg_replace_callback()Tests/Console/Services/Installation/InstallationManagerTest — Copertura regressione valori numericitestInstallWithDatabaseConfig: aggiunta asserzione su DATABASE_PORT; il template di config ora usa valori vuoti ("") per DATABASE_PASSWORD e DATABASE_PORT, replicando lo scenario reale che innescava il bugtestUpdateConfigFileWithNumericValueDoesNotMangle (nuovo, con data provider): verifica che il valore DATABASE_PORT non venga troncato sia con il valore di default (3306) sia con un valore inserito dall'utente (5432); include una negative assertion che esclude la presenza del valore troncatoaskSecret() mantiene la stessa firma e comportamento visibile; su TTY reale il comportamento (echo disabilitato) rimane invariato. updateConfigFile() è un metodo privato interno.
Patch che corregge un comportamento anomalo nel CommandDispatcher: i comandi dei moduli venivano scoperti dopo quelli del framework, impedendo ai moduli di estendere o sovrascrivere i comandi nativi. L'ordine è stato invertito — moduli prima (nell'ordine di MODULE_FOLDERS), framework come fallback — allineando il CommandDispatcher alla stessa logica di precedenza già adottata dal web Dispatcher.
Console/HelperClasses/CommandDispatcher — Ordine di discovery dei comandi
Il metodo discoverCommands() scansionava prima la directory dei comandi del framework (SismaFramework/Console/Commands/) e poi quella dei moduli, nell'ordine inverso rispetto al comportamento atteso. Poiché run() si ferma al primo comando compatibile, qualsiasi comando di un modulo con lo stesso nome di un comando del framework veniva silenziosamente ignorato, rendendo impossibile estendere o sovrascrivere i comandi nativi dall'esterno del framework.
L'ordine è stato corretto: i moduli vengono scansionati per primi, rispettando la sequenza dichiarata in MODULE_FOLDERS; il framework viene aggiunto per ultimo come fallback. Questo rispecchia esattamente la logica di precedenza del web Dispatcher e permette ai moduli di estendere i comandi del framework tramite ereditarietà, chiamando parent::execute() dopo aver aggiunto la propria logica.
File modificati:
Console/HelperClasses/CommandDispatcher.php: in discoverCommands(), il foreach ($this->config->moduleFolders ...) spostato prima della chiamata a discoverFromDirectory() sul path di sistema
Patch di manutenzione che allarga il vincolo su psr/log per consentire l'installazione di phpDocumentor come dipendenza di sviluppo, corregge un potenziale TypeError in SismaLogger::interpolate() con messaggi \Stringable, aggiorna la documentazione Markdown (esempi API errati, sezione OAuth mancante) e rigenera la documentazione phpDocumentor allineandola alle classi introdotte in 11.6.0.
Core/HelperClasses/SismaLogger — Gestione \Stringable in interpolate()
Il metodo privato interpolate() dichiarava string $message come tipo del parametro. Poiché LoggerInterface (psr/log 2.x/3.x) consente di passare oggetti \Stringable ai metodi di log, qualsiasi chiamata con un \Stringable avrebbe generato un TypeError prima di raggiungere il metodo. Il tipo è stato aggiornato a \Stringable|string e viene applicato un cast (string) all'inizio del metodo, garantendo la compatibilità con l'intera gamma di messaggi ammessi dall'interfaccia PSR-3.
File modificati:
Core/HelperClasses/SismaLogger.php: Firma interpolate() aggiornata a \Stringable|string $message; aggiunto $message = (string) $message come prima istruzioneConsole/Services/Installation/InstallationManager — Vincolo psr/log nei nuovi progetti
Il metodo che inietta la dipendenza psr/log nel composer.json dei nuovi progetti impostava il vincolo a ^3.0. Aggiornato a ^2.0 || ^3.0 per allinearlo al vincolo del framework e consentire la coesistenza con phpDocumentor anche nei progetti installati.
File modificati:
Console/Services/Installation/InstallationManager.php: Vincolo iniettato aggiornato da ^3.0 a ^2.0 || ^3.0composer.json — Allargamento vincolo psr/log e aggiunta phpDocumentor
Il vincolo "psr/log": "^3.0" impediva l'installazione di phpDocumentor come require-dev, poiché le sue dipendenze indirette richiedono psr/log ^2.0. Il vincolo è stato allargato a ^2.0 || ^3.0: il codice del framework non usa alcuna API specifica di psr/log 3.x (i metodi LoggerInterface sono implementati senza type hint espliciti su $message, compatibili con tutte e tre le major), quindi l'allargamento non introduce alcun rischio regressivo.
Aggiunto inoltre "config": {"platform": {"php": "8.4.99"}} per permettere la risoluzione delle dipendenze su PHP 8.5 (dove phpdocumentor/json-path — dipendenza indiretta — non dichiara ancora supporto esplicito, pur funzionando correttamente). Aggiunto script "phpdoc": "php vendor/bin/phpdoc --config phpdoc.xml" per semplificare la rigenerazione della documentazione API.
File modificati:
composer.json: psr/log aggiornato a ^2.0 || ^3.0; aggiunto phpdocumentor/phpdocumentor: ^3.10 in require-dev; aggiunte sezioni config e scriptsTests/Console/Services/Installation/InstallationManagerTest — Allineamento asserzioni
Le due asserzioni che verificavano il valore del vincolo psr/log iniettato da InstallationManager sono state aggiornate da '^3.0' a '^2.0 || ^3.0'.
File modificati:
Tests/Console/Services/Installation/InstallationManagerTest.php: Due assertEquals('^3.0', ...) aggiornatidocs/security.md — Correzione esempi API e aggiunta sezione OAuth
La sezione di esempio per l'autenticazione form-based conteneva riferimenti a metodi inesistenti nell'API pubblica (isLogged(), login()) e a un pattern logicamente scorretto (checkAuthenticable() && checkPassword(), dove checkPassword() è già chiamato internamente da checkAuthenticable()). Corretti anche gli accessi alle proprietà di Request (da notazione ad oggetto ->get() a accesso array ['key'], coerente con la definizione della classe) e il nome del metodo getAuthenticable() → getAuthenticableInterface().
Aggiunta sezione completa Autenticazione OAuth 2.0 che documenta OAuthAuthentication, OAuthWrapperInterface, il flusso Authorization Code in due fasi e un esempio di implementazione di un wrapper provider.
File modificati:
docs/security.md: Corretti esempi form-based; aggiunta sezione OAuthdocs/forms.md — Correzione nome metodo getFilterErrors()
L'esempio del controller utilizzava $form->returnFilterErrors(), metodo inesistente. Corretto in $form->getFilterErrors() (metodo ereditato da SubmittableTrait).
File modificati:
docs/forms.md: returnFilterErrors() → getFilterErrors()docs/controllers.md — Correzione esempio autowiring Authentication
L'esempio di autowiring utilizzava $auth->isLogged() (metodo inesistente), il namespace errato SismaFramework\Security\Authentication e l'accesso alle proprietà di Request tramite ->get(). Corretti namespace, metodo di verifica sessione e accesso array.
File modificati:
docs/controllers.md: Namespace, controllo sessione e accesso Request correttidocs/api-reference.md — Correzione firme BaseForm e aggiunta sezioni Security/HTTP
getErrors(): FormFilterErrorCollection era il nome errato del metodo (corretto in getFilterErrors(): FormFilterError); la firma di handleRequest() mancava del parametro Request $request. Aggiunte le sezioni Security Classes (Authentication, OAuthAuthentication, OAuthWrapperInterface, BaseVoter, BasePermission) e HTTP Classes (Response), che erano elencate nell'indice del documento ma mai implementate nel corpo.
File modificati:
docs/api-reference.md: Firme BaseForm corrette; sezioni Security e HTTP aggiuntedocs-phpdoc/ — Rigenerazione completa
Rigenerata da zero tramite composer phpdoc per includere le nuove classi introdotte in 11.6.0 (OAuthAuthentication, OAuthWrapperInterface, BaseAuthentication, SubmittableTrait) ed eliminare il file orfano SismaFramework-Core-AbstractClasses-Submittable.html, rimasto dalla generazione precedente dopo la rimozione del file PHP sorgente.
@deprecated — versione di introduzione e rimozione
Quattro classi/metodi presentavano annotazioni @deprecated incomplete o errate: mancavano la versione in cui la deprecazione era stata introdotta, la versione di rimozione pianificata, oppure il testo era in inglese anziché italiano, creando incoerenza con il resto della codebase.
Orm/ExtendedClasses/DependentModel e Orm/ExtendedClasses/SelfReferencedModel — i tre metodi deprecati (countEntityCollectionByEntity, getEntityCollectionByEntity, deleteEntityCollectionByEntity) riportavano dalla versione 11.0.0, ma la deprecazione era stata introdotta in v10.1.0 (commit 9c9f5ed4, 2025-11-21). Corretto in dalla versione 10.1.0; aggiunta la versione di rimozione pianificata 12.0.0.
Security/ExtendedClasses/LogException e Security/ExtendedClasses/NoLogException — le annotazioni erano in inglese e prive di numeri di versione. La deprecazione è stata introdotta in v11.0.0 (commit 87843e03, 2025-12-18). Aggiunta versione di introduzione 11.0.0, versione di rimozione 12.0.0; testo armonizzato in italiano coerentemente con gli altri messaggi di deprecazione del framework.
File modificati:
Orm/ExtendedClasses/DependentModel.php: versione @deprecated corretta da 11.0.0 a 10.1.0; aggiunto sarà rimosso nella versione 12.0.0 (3 metodi)Orm/ExtendedClasses/SelfReferencedModel.php: stessa correzione (3 metodi)Security/ExtendedClasses/LogException.php: annotazione @deprecated riscritta con versioni e in italianoSecurity/ExtendedClasses/NoLogException.php: annotazione @deprecated riscritta con versioni e in italianoSismaLogger rimangono invariate; il comportamento di interpolate() è identico per input di tipo string (il 100% dei casi d'uso interni).
Rifattorizzazione interna del sistema di autenticazione: la classe astratta Submittable è stata convertita in un trait, e il comportamento comune a tutte le classi di autenticazione è stato estratto nella nuova classe astratta BaseAuthentication. Il refactoring ha abilitato l'implementazione di OAuthAuthentication, che supporta il flusso Authorization Code OAuth 2.0 senza SubmittableTrait poiché in OAuth non esiste un form da sottomettere né errori di validazione da riportare al template.
Core/Traits/SubmittableTrait — Conversione da classe astratta a trait
Submittable era una classe astratta @internal usata come base sia da Authentication che da BaseForm, pur non rappresentando un tipo condiviso tra le due gerarchie, bensì un comportamento ortogonale (rilevamento form submission). È stata convertita in un trait e spostata in Core/Traits/.
Il trait espone:
protected FormFilterError $formFilterErrorprotected function initSubmittable(): void — da chiamare nel costruttore della classe utilizzatricepublic function isSubmitted(): boolpublic function getFilterErrors(): FormFilterErrorFile modificati:
Core/Traits/SubmittableTrait.php (nuovo): Implementazione del trait, marcato @internalCore/AbstractClasses/Submittable.php (eliminato)Security/BaseClasses/BaseAuthentication — Nuova classe astratta base per l'autenticazione
Estratta da Authentication la logica comune a qualsiasi flusso di autenticazione (form-based, OAuth, ecc.). La nuova classe astratta BaseAuthentication, marcata @internal, centralizza:
protected Request $requestprotected Filter $filterprotected Session $sessionprotected ?AuthenticableInterface $authenticableInterfacepublic function getAuthenticableInterface(): AuthenticableInterface
SubmittableTrait non è incluso in BaseAuthentication perché non tutti i flussi di autenticazione hanno un form: Authentication (form-based) lo usa, OAuthAuthentication no.
File modificati:
Security/BaseClasses/BaseAuthentication.php (nuovo): Classe astratta base, marcata @internalSecurity/HttpClasses/Authentication — Adeguamento alla nuova gerarchia
Authentication passa da extends Submittable a extends BaseAuthentication con use SubmittableTrait. Le property $filter, $session, $authenticableInterface e il metodo getAuthenticableInterface() sono stati spostati in BaseAuthentication. Il costruttore chiama parent::__construct() e $this->initSubmittable().
File modificati:
Security/HttpClasses/Authentication.php: Aggiornamento gerarchia e rimozione membri ora in BaseAuthenticationCore/BaseClasses/BaseForm — Adeguamento al SubmittableTrait
BaseForm passa da extends Submittable a use SubmittableTrait, dichiarando protected Request $request direttamente nella classe. Il costruttore sostituisce parent::__construct() con $this->initSubmittable().
File modificati:
Core/BaseClasses/BaseForm.php: Sostituzione ereditarietà con trait; dichiarazione esplicita di $requestSecurity/HttpClasses/OAuthAuthentication — Autenticazione OAuth 2.0 Authorization Code Flow
Nuova classe OAuthAuthentication extends BaseAuthentication che implementa il flusso Authorization Code OAuth 2.0. Non usa SubmittableTrait perché in OAuth non esiste un form da sottomettere: gli errori arrivano come parametri URL dal provider e vengono gestiti tramite valori di ritorno ed eccezioni, non tramite FormFilterError.
Il flusso si articola in due fasi:
Fase 1 — Redirect al provider:
getAuthorizationUrl(): string genera uno state casuale con random_bytes, lo persiste in sessione e delega la costruzione dell'URL a OAuthWrapperInterface::getAuthorizationUrl().Fase 2 — Callback dal provider:
checkCallback(): bool verifica la presenza di errori del provider ($request->query['error']), convalida lo state in modo timing-safe tramite hash_equals(), scambia il code per un identificatore utente tramite OAuthWrapperInterface::getAuthenticableIdentifier() e recupera l'entità autenticabile tramite AuthenticableModelInterface.
La protezione CSRF del callback segue lo stesso pattern difensivo di Authentication::checkCsrfToken(): verifica sequenziale con early return.
File modificati:
Security/HttpClasses/OAuthAuthentication.php (nuovo)Security/Interfaces/Wrappers/OAuthWrapperInterface — Contratto per i provider OAuthNuova interfaccia che astrae la comunicazione con il provider OAuth. Ogni provider (Google, GitHub, ecc.) implementa:
getAuthorizationUrl(string $state): string — costruisce l'URL di autorizzazione con il parametro stategetAuthenticableIdentifier(string $code): string — scambia il codice di autorizzazione per un identificatore utente (es. email); eventuali errori di rete o token invalidi propagano come eccezioni al chiamanteFile modificati:
Security/Interfaces/Wrappers/OAuthWrapperInterface.php (nuovo)Tests/Security/HttpClasses/OAuthAuthenticationTest — Copertura completa del flusso OAuth
Sette test che coprono tutti i percorsi di checkCallback() e getAuthorizationUrl():
testGetAuthorizationUrl — verifica che lo state venga scritto in sessione e che l'URL venga restituito dal wrappertestCheckCallbackWithProviderError — early return false in presenza di error nella query stringtestCheckCallbackWithMissingSessionState — false se lo state non è presente in sessionetestCheckCallbackWithMissingRequestState — false se lo state manca nella query stringtestCheckCallbackWithMismatchedState — false se gli state non corrispondonotestCheckCallbackWithMissingCode — false se il code manca dalla query stringtestCheckCallbackWithUserNotFound — false se il modello non trova l'utentetestCheckCallbackSuccess — true con verifica di getAuthenticableInterface()File modificati:
Tests/Security/HttpClasses/OAuthAuthenticationTest.php (nuovo)Authentication e BaseForm sono invariate. Submittable era marcata @internal by design e non esposta come API consumabile dall'esterno del framework.OAuthAuthentication e OAuthWrapperInterface sono addizioni pure: nessuna classe esistente è modificata dalla loro introduzione.Piccola rifattorizzazione del template del controller generato dal comando di scaffolding, per semplificare eventuali personalizzazioni post-generazione.
Console/Services/Scaffolding/Templates/Controller.tpl — Estrazione variabile entità prima del salvataggio
Nelle azioni create e update, la chiamata a resolveEntity() era concatenata direttamente come argomento di $this->dataMapper->save() su un'unica riga. L'entità risolta viene ora assegnata a una variabile dedicata prima di essere passata al DataMapper.
$this->dataMapper->save(${{entityShortNameLower}}Form->resolveEntity()); ${{entityShortNameLower}} = ${{entityShortNameLower}}Form->resolveEntity();
$this->dataMapper->save(${{entityShortNameLower}});
Questo rende il codice generato più leggibile e facilita eventuali personalizzazioni (es. manipolare l'entità tra resolveEntity() e save()), senza alcuna modifica al comportamento a runtime.
Rimossi inoltre i trailing whitespace sulle righe vuote tra i metodi della classe.
File modificati:
Console/Services/Scaffolding/Templates/Controller.tpl: Estrazione variabile entità nelle azioni create e update; pulizia trailing whitespaceQuesta patch corregge due bug nel template del controller generato dal comando di scaffolding.
Console/Services/Scaffolding/Templates/Controller.tpl — Namespace modello errato e metodo form scorretto
Bug 1 — Namespace use del modello con segmento Models duplicato
Il namespace nell'istruzione use includeva un segmento \Models\ ridondante: poiché {{modelNamespace}} contiene già il segmento Models, il risultato era una duplicazione (es. …\Models\Models\{{entityShortName}}Model), producendo un'istruzione non valida nel controller generato.
use {{modelNamespace}}\Models\{{entityShortName}}Model;use {{modelNamespace}}\{{entityShortName}}Model;
Bug 2 — Uso di getEntity() al posto di resolveEntity() nelle azioni create ed edit
Il salvataggio dell'entità nelle azioni create ed edit chiamava getEntity(), che non risolve correttamente le relazioni del form. Il metodo corretto è resolveEntity().
$this->dataMapper->save(${{entityShortNameLower}}Form->getEntity());$this->dataMapper->save(${{entityShortNameLower}}Form->resolveEntity());File modificati:
Console/Services/Scaffolding/Templates/Controller.tpl: Corretto namespace use del modello; sostituito getEntity() con resolveEntity() nelle azioni create ed editsisma
Questa minor aggiunge il supporto nativo al comando sisma su Windows e semplifica l'avvio su Linux/macOS tramite shebang.
Console/sisma — Aggiunto shebang #!/usr/bin/env php
Lo script sisma può ora essere invocato direttamente da terminale su Linux e macOS (es. sisma fixtures) senza anteporre php, grazie alla riga shebang. PHP ignora la riga #! quando il file viene eseguito tramite php sisma, garantendo piena retrocompatibilità.
File modificati:
Console/sisma: Aggiunta riga #!/usr/bin/env php come prima riga del fileConsole/sisma.bat — Nuovo wrapper per Windows
Aggiunto file sisma.bat nella stessa directory di sisma, che consente di invocare il comando come sisma fixtures anche su Windows nativo (senza Docker). Windows riconosce automaticamente l'estensione .bat quando il nome del comando è nel PATH.
File aggiunti:
Console/sisma.bat: Wrapper @php "%~dp0sisma" %*php sisma <comando> continua a funzionare invariato su qualsiasi piattaforma.enableErrorDisplay() in ErrorHandler
Questa patch consolida la gestione del bootstrap nei due entry point del framework. Il require_once dell'autoload di Composer viene spostato direttamente nello skeleton di index.php, semplificando la procedura d'installazione. La logica di abilitazione degli errori viene estratta in un metodo statico di ErrorHandler, eliminando la dipendenza da LoggerInterface nel contesto di bootstrap della console.
ErrorHandler — Estrazione di enableErrorDisplay() come metodo statico
La versione 11.4.0 aveva introdotto showErrorInDevelopmentEnvironment() come metodo d'istanza, usato anche nello script sisma. Questo richiedeva l'istanziazione di ErrorHandler e quindi la dipendenza da Psr\Log\LoggerInterface (via vendor autoload) già in fase di bootstrap della console, prima ancora di qualsiasi comando. Il blocco ini_set è stato estratto nel nuovo metodo statico enableErrorDisplay(), senza dipendenze esterne, riutilizzabile sia da sisma che internamente da showErrorInDevelopmentEnvironment().
Modifica:
$errorHandler = new ErrorHandler(); $errorHandler->showErrorInDevelopmentEnvironment(); in sisma (richiede vendor autoload)ErrorHandler::enableErrorDisplay(); in sisma (metodo statico, nessuna istanza, nessuna dipendenza vendor); showErrorInDevelopmentEnvironment() delega internamente a self::enableErrorDisplay()File modificati:
Core/HelperClasses/ErrorHandler.php: Aggiunto metodo statico enableErrorDisplay(); showErrorInDevelopmentEnvironment() ora chiama self::enableErrorDisplay() al posto del blocco ini_set inlineConsole/sisma: Sostituito blocco ini_set con ErrorHandler::enableErrorDisplay(); rimosso require_once vendor/autoload.phpPublic/index.php — vendor/autoload.php incluso nello skeleton
Il require_once dell'autoload di Composer veniva iniettato dinamicamente da InstallationManager::copyPublicFolder() tramite manipolazione di stringa sul file copiato. Poiché il percorso relativo dirname(__DIR__) . '/vendor/autoload.php' è invariante sia nello skeleton (SismaFramework/vendor/) sia nel progetto installato (projectRoot/vendor/), la riga è ora inclusa direttamente nel file sorgente. L'ordine di caricamento è: autoload SismaFramework prima, autoload vendor dopo.
File modificati:
Public/index.php: Aggiunto require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; dopo il require dell'autoload internoConsole/Services/Installation/InstallationManager.php: Rimossa la logica di iniezione dinamica della riga vendor/autoload.php in copyPublicFolder()showErrorInDevelopmentEnvironment() mantiene la stessa firma e semantica.
Questa minor introduce l'auto-discovery automatico dei comandi console tramite il pattern factory nel CommandDispatcher, allineando l'architettura della console a quella del Dispatcher HTTP. I comandi non devono più essere registrati manualmente nello script sisma: vengono scoperti automaticamente sia nel framework che in tutti i moduli configurati.
CommandDispatcher — Auto-discovery dei comandi via factory
Il CommandDispatcher registrava i comandi esclusivamente tramite chiamate esplicite ad addCommandStrategy() nello script di avvio. Questo approccio richiedeva la modifica manuale del file sisma ad ogni aggiunta di un nuovo comando, sia nel framework che nei moduli applicativi.
Ora il costruttore invoca internamente discoverCommands(), che scansiona via reflection le directory Console/Commands/ del framework e di tutti i moduli presenti in Config::$moduleFolders, istanziando automaticamente ogni classe concreta che estende BaseCommand tramite la nuova classe CommandFactory.
Modifica:
$commandDispatcher->addCommandStrategy(new FooCommand())CommandDispatcherFile modificati:
Console/HelperClasses/CommandDispatcher.php: Aggiunta dipendenza opzionale Config, aggiunti i metodi privati discoverCommands() e discoverFromDirectory(); il costruttore accetta ora un secondo parametro opzionale ?Config $config = nullConsole/HelperClasses/Dispatcher/CommandFactory.php: Nuova classe factory che istanzia comandi tramite reflection, con risoluzione automatica delle dipendenze non primitive del costruttoreConsole/sisma — Rimozione registrazione manuale dei comandi
Lo script di avvio della console registrava esplicitamente tutti i comandi del framework (FixturesCommand, InstallationCommand, ScaffoldCommand, UpgradeCommand) tramite addCommandStrategy(). Con l'auto-discovery queste chiamate sono ridondanti e sono state rimosse.
Sostituito inoltre il blocco error_reporting / ini_set con l'utilizzo di ErrorHandler::showErrorInDevelopmentEnvironment(), in linea con il resto del framework.
File modificati:
Console/sisma: Rimossi i use e le chiamate addCommandStrategy() per i quattro comandi nativi; rimosso blocco error_reporting/ini_set sostituito da $errorHandler->showErrorInDevelopmentEnvironment()addCommandStrategy() è ancora disponibile per la registrazione manuale di comandi aggiuntivi. Il parametro $config del costruttore è opzionale e retrocompatibile.
Questa patch introduce l'esecuzione atomica delle fixtures tramite una transazione globale nel FixturesManager, e corregge il comportamento del TransactionManager::rollback() che non verificava lo stato attivo della transazione prima di eseguire il rollback sul database.
TransactionManager::rollback() — Guardia su transazione attiva
Il metodo rollback() eseguiva $this->adapter->rollbackTransaction() incondizionatamente, senza verificare se una transazione fosse effettivamente aperta. Questo poteva causare un errore del driver database in caso di chiamata su connessione senza transazione attiva.
Modifica:
$this->adapter->rollbackTransaction(); (incondizionato)self::$isActiveTransaction === true, con reset del flag dopo il rollbackFile modificati:
Orm/HelperClasses/DataMapper/TransactionManager.php: Aggiunta guardia if (self::$isActiveTransaction) e reset di $isActiveTransaction = false in rollback()FixturesManager::run() — Esecuzione atomica tramite transazione globale
L'esecuzione delle fixtures avveniva senza una transazione globale: ogni save() apriva e chiudeva la propria transazione autonomamente. In caso di errore a metà esecuzione, i record già inseriti dai fixture precedenti rimanevano nel database.
Ora run() apre una transazione prima di eseguire i fixture e la committa solo al termine di tutti. Se un save() fallisce internamente, esegue il rollback dell'intera transazione e rilancia l'eccezione, che propaga naturalmente al sisma script.
File modificati:
Console/Services/Fixtures/FixturesManager.php: Aggiunte chiamate startTransaction() prima di executeFixturesArray() e commitTransaction() dopoConsole/sisma — Path con DIRECTORY_SEPARATOR
Il file di avvio della console usava / hardcoded per costruire i path di configurazione e autoload, causando potenziali problemi su sistemi Windows.
File modificati:
Console/sisma: Sostituiti i separatori / hardcoded con DIRECTORY_SEPARATOR nei path di configFramework.php, config.php e autoload.phpScaffoldingManagerTest::testDoubleExecution — Path con DIRECTORY_SEPARATOR
Il messaggio di eccezione atteso nel test usava \ hardcoded per il path, causando il fallimento del test su sistemi Linux/macOS dove il separatore è /.
File modificati:
Tests/Console/Services/Scaffolding/ScaffoldingManagerTest.php: Sostituiti i separatori \ hardcoded con DIRECTORY_SEPARATOR nei messaggi di eccezione attesi
Questa patch ripristina il codice precedente nel metodo setApplicationModuleByClassName() della classe ModuleManager, rimuovendo l'uso di array_first() introdotto involontariamente nella versione 11.3.4. La funzione array_first() è disponibile solo a partire da PHP 8.5, incompatibile con il requisito minimo del framework (PHP 8.3).
ModuleManager::setApplicationModuleByClassName()
Il commit della versione 11.3.4 aveva sostituito $classNameParts[0] con array_first($classNameParts), funzione introdotta in PHP 8.5 e non disponibile in PHP 8.3 e 8.4.
Ripristino:
self::setApplicationModule(array_first($classNameParts));$module = $classNameParts[0]; / self::setApplicationModule($module);File modificati:
Core/HelperClasses/ModuleManager.php: Ripristinato accesso tramite indice array
Questa patch release corregge un bug per cui la classe ErrorHandler non impostava il modulo applicativo prima di invocare i controller di errore, causando un fallimento nella risoluzione delle view di errore.
I metodi pubblici di ErrorHandler chiamavano i controller di errore senza prima impostare il modulo tramite ModuleManager, a differenza di quanto avviene nel Dispatcher. Il sistema di rendering non riusciva quindi a individuare il percorso corretto delle view.
Modifiche applicate:
registerNonThrowableErrorHandler(): Aggiunta chiamata ModuleManager::setApplicationModuleByClassName(get_class($controller)) all'inizio della shutdown function, prima di invocare il controller di errore non-throwable.handleBaseException(): Aggiunta chiamata ModuleManager::setApplicationModuleByClassName() in entrambi i branch (developmentEnvironment e produzione), rispettivamente con $structuralController e $defaultController come sorgente del modulo.handleThrowableError(): Aggiunta chiamata ModuleManager::setApplicationModuleByClassName(get_class($structuralController)) dopo BufferManager::clear(), prima del log e dell'invocazione del controller.File modificati:
Core/HelperClasses/ErrorHandler.php: Aggiunte 4 chiamate a ModuleManager::setApplicationModuleByClassName() nei metodi di gestione erroriBackupManagerTest con estensione ZIP non disponibile
Il tearDown() accedeva alla proprietà tipizzata $testDir anche quando setUp() aveva chiamato markTestSkipped() prima di inizializzarla (assenza dell'estensione ZIP), causando un errore Typed property must not be accessed before initialization.
Fix: l'assegnazione di $testDir è stata spostata prima del controllo sull'estensione, garantendo che la proprietà sia sempre inizializzata prima che tearDown() venga eseguito.
File modificati:
Tests/Console/Services/Upgrade/Utils/BackupManagerTest.php: Spostata l'inizializzazione di $testDir prima di markTestSkipped()
Questa patch release corregge un errore introdotto nella versione 11.3.2, dove la proprietà $project era stata erroneamente rimossa dalla classe Config nonostante venisse ancora utilizzata dal FrameworkController.
Config::$project
La proprietà $project era stata inclusa per errore nell'elenco delle proprietà "orfane" rimosse nella versione 11.3.2. In realtà viene letta a runtime in FrameworkController::throwableError() e FrameworkController::nonThrowableError() per popolare la variabile di template $vars['project'] nella pagina di errore visibile.
File modificati:
Core/HelperClasses/Config.php: Ripristinata la proprietà protected readonly string $projectQuesta patch release rifattorizza il sistema di esecuzione delle fixtures, spostandolo dal contesto HTTP (Dispatcher) al contesto CLI (Console). Il comportamento delle fixtures resta invariato: cambiano solo il punto di invocazione e la collocazione del codice. Include inoltre la documentazione del sistema di Upgrade introdotto nella versione 11.3.0.
Le fixtures erano una funzionalità nata quando il framework non disponeva di una console CLI. Venivano eseguite tramite un endpoint HTTP (/fixtures), integrato nel Dispatcher e nel RouteResolver. Con l'introduzione della console, questa collocazione risultava architetturalmente inadeguata.
Nuovo comando CLI:
php SismaFramework/Console/sisma fixtures
File creati:
Console/Commands/FixturesCommand.php: Nuovo comando che estende BaseCommand, registrato nel file sismaConsole/Services/Fixtures/FixturesManager.php: Logica di gestione fixtures spostata dal Dispatcher alla ConsoleFile modificati:
Console/sisma: Registrato FixturesCommand tra le strategie del CommandDispatcherCore/HelperClasses/Dispatcher.php: Rimosso il branch elseif per le fixtures nel metodo handle()Core/HelperClasses/Dispatcher/RouteResolver.php:FixturesManager nel costruttoreisFixturesRequest() e runFixtures()isFixtures() in parsePathWithMultipleCleanParts()Core/HelperClasses/Config.php: Rimossa la proprietà $fixtures (nome della route HTTP, non più necessario)File eliminati:
Core/HelperClasses/Dispatcher/FixturesManager.php: Sostituito dalla versione in Console/Services/Fixtures/Modifiche al FixturesManager:
run() ora restituisce void invece di Response (non essendo più in contesto HTTP)isFixtures() (non più necessario senza routing HTTP)SismaFramework\Core\HelperClasses\Dispatcher a SismaFramework\Console\Services\Fixtures
Rimosse 10 proprietà dalla classe Config che non venivano mai lette a runtime tramite $config->proprietà. Queste proprietà esistevano come mapping delle corrispondenti costanti in config.php, ma nessun codice PHP le accedeva — le costanti servono esclusivamente come building block per la composizione di altre costanti e restano invariate.
Proprietà rimosse: $adapters, $assets, $cache, $core, $defaultController, $logs, $project, $resources, $thisDirectory, $directoryUp
Tests/Console/Services/Fixtures/FixturesManagerTest.php: Test spostato dal contesto Core al contesto ConsoleTests/Core/HelperClasses/DispatcherTest.php: Rimosso testRunFixture e tutti i riferimenti a FixturesManagerTests/Core/HelperClasses/DebuggerTest.php: Rimosso l'attributo #[RunTestsInSeparateProcesses] per incompatibilità con PHPUnit 12Tests/Core/HelperClasses/ConfigTest.php: Aggiornati i test di reflection per riflettere la rimozione delle proprietà orfanedocs/upgrade.md: Aggiunta guida completa al sistema di Upgrade automatico introdotto nella versione 11.3.0docs/index.md: Aggiunto riferimento alla nuova pagina di documentazionedocs/data-fixtures.md: Aggiornata la sezione "Eseguire le Fixtures" da URL browser a comando CLIdocs/helper-classes.md: Rimossa la sezione FixturesManager e il relativo riferimento nella tabella panoramicadocs/getting-started.md: Aggiornate le istruzioni di esecuzione fixtures da URL a comando CLIdocs/testing.md: Aggiornato il namespace di FixturesManager e rimosso l'uso del metodo isFixtures() nell'esempiodocs/configuration-reference.md: Rimosso il riferimento alle fixtures dalla descrizione di DEVELOPMENT_ENVIRONMENTBaseFixture, setEntity(), setDependencies()) restano invariateGET /fixtures (browser) a php sisma fixtures (terminale)
Questa patch release corregge un bug nell'Autoloader che impediva il caricamento delle classi mappate tramite AUTOLOAD_NAMESPACE_MAPPER e AUTOLOAD_CLASS_MAPPER su sistemi Linux/macOS.
Corretti i metodi mapNamespace() e mapClass() in Autoloader.php per convertire correttamente i backslash nei percorsi provenienti dalle costanti di configurazione:
mapNamespace()):$this->config->rootPath . $value . str_replace('\\', DIRECTORY_SEPARATOR, $actualClassName) . '.php'$this->config->rootPath . str_replace('\\', DIRECTORY_SEPARATOR, $value . $actualClassName) . '.php'str_replace('\\', DIRECTORY_SEPARATOR, ...) veniva applicata solo a $actualClassName, ma non a $value (il percorso dalla configurazione)mapClass()):$this->config->rootPath . $this->config->autoloadClassMapper[$this->className] . '.php'$this->config->rootPath . str_replace('\\', DIRECTORY_SEPARATOR, $this->config->autoloadClassMapper[$this->className]) . '.php'Scenario del bug:
AUTOLOAD_NAMESPACE_MAPPER contiene percorsi con backslash (es. "plugins\PHPMailer\src")DIRECTORY_SEPARATOR è //var/www/html/plugins\PHPMailer\src/PHPMailer.phpfile_exists() falliva perché il percorso non era valido su Linux
AUTOLOAD_NAMESPACE_MAPPER e AUTOLOAD_CLASS_MAPPER vengono convertiti in DIRECTORY_SEPARATOR/var/www/html/plugins/PHPMailer/src/PHPMailer.php
Questa release introduce un sistema completo di upgrade automatico che consente di migrare moduli tra versioni major del framework applicando automaticamente le trasformazioni necessarie per i breaking changes. Inoltre, viene aggiunto il parametro TextSearchMode ai metodi di ricerca fulltext dell'ORM, consentendo un controllo esplicito sulla modalità di ricerca testuale.
upgrade per automatizzare la migrazione dei moduli tra versioni majorConsole/Commands/UpgradeCommand.php): Entry point CLI con supporto completo per opzioni--to=VERSION: Versione target (obbligatorio)--from=VERSION: Versione sorgente (auto-rilevata da module.json se omesso)--dry-run: Preview delle modifiche senza applicarle (raccomandato)--skip-critical: Salta file critici (Public/index.php, Config)--skip-backup: Salta backup automatico (non raccomandato)--quiet: Output minimaleConsole/Services/Upgrade/UpgradeManager.php): Orchestrator principaleUpgradeStrategyInterface: Interfaccia per strategie di upgradeUpgrade10to11Strategy: Implementazione per migrazione 10.x → 11.0.0TransformerInterface: Interfaccia base per trasformatori di codiceStaticToInstanceTransformer: Converte chiamate statiche a istanze (ErrorHandler, Debugger)ReturnTypeTransformer: Aggiorna signature customFilter() da void a bool + return statementsResponseConstructorTransformer: Converte setResponseType() a constructor injectionMethodRenameTransformer: Rinomina metodi deprecatiVersionDetector: Rilevamento e aggiornamento versione framework in module.jsonFileScanner: Scansione intelligente dei file del modulo con categorizzazioneBackupManager: Creazione backup ZIP + git commit (se disponibile)ReportGenerator: Report dettagliati con confidence score e warningTransformationResult: Risultato di una trasformazione con confidence, warning, modificheUpgradeReport: Report completo di upgrade con statistiche e azioni manuali richiestemodule.json per tracciare la versione framework di ogni modulo {
"name": "ModuleName",
"version": "1.0.0",
"framework_version": "11.0.0",
"description": "Module description",
"authors": ["Author Name"],
"requires": {
"sismaframework": ">=11.0.0"
}
}
| Trasformazione | Confidence | Note | |----------------|------------|------| | Static→Instance | 70-75% | Alta per index.php, warning per altri file | | ReturnType void→bool | 80-85% | Rilevamento automatico indentazione | | Response constructor | 65-70% | Warning per pattern complessi | | Method renaming | 90% | Alta affidabilità |
UpgradeException: Eccezione generica per errori di upgradeVersionMismatchException: Versione non valida o strategia non trovataBackupFailedException: Errore durante backup o rollback# Preview upgrade (raccomandato come primo step)
php Console/sisma upgrade Blog --to=11.0.0 --dry-run
# Applicazione upgrade dopo review
php Console/sisma upgrade Blog --to=11.0.0
# Upgrade da versione specifica
php Console/sisma upgrade Blog --from=10.1.7 --to=11.0.0
# Salta file critici per review manuale
php Console/sisma upgrade Blog --to=11.0.0 --skip-critical
# Output minimale
php Console/sisma upgrade Blog --to=11.0.0 --quiet
Il sistema è progettato per essere facilmente estensibile:
Upgrade11to12Strategy.php implementando UpgradeStrategyInterfaceTransformerInterface
TextSearchMode alla ricerca fulltext
Aggiunto il parametro TextSearchMode ai metodi di ricerca fulltext per consentire un controllo esplicito sulla modalità di ricerca testuale:
setFulltextIndexColumn()):TextSearchMode $textSearchMode = TextSearchMode::inNaturaLanguageModeopFulltextIndex(): aggiunto parametro obbligatorio TextSearchMode $textSearchMode, rimossi valori di default dai parametri $value e $columnAliasfulltextConditionSintax(): rimosso valore di default dal parametro TextSearchMode $textSearchMode, rendendolo obbligatorioopFulltextIndex() e fulltextConditionSintax() per propagare il parametro TextSearchMode@internal alla classeMotivazione:
TextSearchMode, delegando la scelta al livello superiore (Query)Questa minor release aggiorna i requisiti minimi del framework a PHP 8.3 e PHPUnit 12, allineandosi con le versioni attivamente supportate e sfruttando le feature moderne del linguaggio già presenti nel codebase. Inoltre, il processo di installazione ora crea automaticamente la struttura del modulo applicativo.
Il comando install ora crea automaticamente la struttura completa del modulo applicativo:
initializeModule($projectName) nel metodo install(){$projectName}/Application/Struttura creata automaticamente:
MyProject/
├── Application/
│ ├── Controllers/
│ ├── Entities/
│ ├── Enumerations/
│ ├── Forms/
│ ├── Models/
│ └── Views/
├── Config/
│ └── configFramework.php
├── Public/
│ └── index.php
├── Cache/
├── Logs/
├── filesystemMedia/
├── .htaccess
└── composer.json
Vantaggi:
"php": ">=8.1.0""php": ">=8.3.0"Motivazione:
#[\Override] (introdotto in PHP 8.3) in 97 file"phpunit/phpunit": "^10.0""phpunit/phpunit": "^12.0"Motivazione:
#[\Override]
Aggiunti gli attributi #[\Override] mancanti ai metodi che sovrascrivono metodi di classi parent o implementano metodi di interfacce:
offsetGet()equals()equals()equals()__call()current(), next(), key(), rewind(), valid() (interfaccia \Iterator)emergency(), alert(), critical(), error(), warning(), notice(), info(), debug(), log())getLog(), getLogRowByRow(), getLogRowNumber(), clearLog() (interfaccia LogReaderInterface)error(), notify() (interfaccia DefaultControllerInterface)Vantaggi:
#[\Override] non sovrascrive effettivamente un metodo parentAggiornati i test per conformarsi alle best practice di PHPUnit 12:
createMock() con createStub(): Nei test dove non vengono configurate expectations sui mock objects, è stato utilizzato createStub() invece di createMock() per evitare PHPUnit notices:BaseControllerTest.phpErrorHandlerTest.phpRenderServiceTest.phpRouterServiceTest.phpTests/configuration.xmlphpunit.xml (nella root del framework)phpunit.xml:Tests/bootstrap.phpVantaggi:
phpunit.xml nella root senza necessità di specificare -c./vendor/bin/phpunit (senza parametri aggiuntivi)# Esecuzione test (PHPUnit trova automaticamente phpunit.xml)
./vendor/bin/phpunit
# Esecuzione test senza code coverage
./vendor/bin/phpunit --no-coverage
# Esecuzione test con coverage (richiede Xdebug)
XDEBUG_MODE=coverage ./vendor/bin/phpunit
Tutti i riferimenti alla versione PHP sono stati aggiornati da 8.1 a 8.3 nei seguenti file:
| File | Tipo | Descrizione |
|---|---|---|
composer.json | Modificato | Requisiti PHP ≥8.3, PHPUnit ^12.0 |
.gitignore | Modificato | Aggiunto composer.lock |
Console/Services/Installation/InstallationManager.php | Modificato | Aggiunta chiamata a initializeModule() |
Console/Commands/InstallationCommand.php | Modificato | Aggiornato output installazione |
Core/CustomTypes/FormFilterErrorCollection.php | Modificato | Aggiunto #[\Override] |
Core/HelperClasses/SismaLogger.php | Modificato | Aggiunto #[\Override] ai metodi PSR-3 |
Core/HelperClasses/SismaLogReader.php | Modificato | Aggiunto #[\Override] |
Orm/CustomTypes/SismaDate.php | Modificato | Aggiunto #[\Override] |
Orm/CustomTypes/SismaDateTime.php | Modificato | Aggiunto #[\Override] |
Orm/CustomTypes/SismaTime.php | Modificato | Aggiunto #[\Override] |
Orm/BaseClasses/BaseResultSet.php | Modificato | Aggiunto #[\Override] ai metodi Iterator |
Orm/ExtendedClasses/SelfReferencedModel.php | Modificato | Aggiunto #[\Override] |
phpunit.xml | Nuovo (rinominato) | Configurazione PHPUnit spostata nella root |
Tests/configuration.xml | Rimosso | Sostituito da phpunit.xml |
Tests/Console/Services/Installation/InstallationManagerTest.php | Modificato | Aggiunto test per verifica creazione modulo |
Tests/Core/BaseClasses/BaseControllerTest.php | Modificato | createMock → createStub |
Tests/Core/HelperClasses/ErrorHandlerTest.php | Modificato | createMock → createStub |
Tests/Core/Services/RenderServiceTest.php | Modificato | createMock → createStub |
Tests/Core/Services/RouterServiceTest.php | Modificato | createMock → createStub |
TestsApplication/Controllers/SampleController.php | Modificato | Aggiunto #[\Override] |
Questa minor release aggiunge la possibilità di configurare i parametri del database in modo interattivo durante l'installazione del framework, migliorando l'esperienza utente senza compromettere la retrocompatibilità.
Aggiunto nuovo trait InteractiveInputTrait per gestire l'input utente dalla console:
ask(string $question, ?string $default = null): string - Richiede input testuale con valore predefinito opzionaleaskConfirmation(string $question, bool $default = true): bool - Richiede conferma Y/NaskSecret(string $question): string - Richiede input senza echo (password)
Migliorato InstallationCommand con richiesta interattiva dei parametri database:
--skip-db per saltare completamente la configurazione database--db-host, --db-name, ecc.) hanno priorità sull'input interattivo# Installazione con richiesta interattiva database
php SismaFramework/Console/sisma install MyProject
# Installazione senza configurazione database
php SismaFramework/Console/sisma install MyProject --skip-db
# Installazione con parametri da command line (comportamento precedente)
php SismaFramework/Console/sisma install MyProject --db-host=localhost --db-name=mydb --db-user=root
Esempio di output interattivo:
Installing SismaFramework project: MyProject
Database Configuration (optional)
Press Enter to skip each field or use defaults.
Do you want to configure database settings? [y/N]: y
Database Host [127.0.0.1]: localhost
Database Port [3306]: 3306
Database Name []: myproject_db
Database Username []: root
Database Password: ********
Creating project structure...
--db-*, l'input interattivo viene saltatoInteractiveInputTrait può essere riutilizzato da altri comandi o moduliaskSecret() non nasconde l'input (limitazione del sistema)askSecret() utilizza stty -echo per nascondere l'input| File | Tipo | Descrizione |
|---|---|---|
Console/Traits/InteractiveInputTrait.php | Nuovo | Trait per input interattivo |
Console/Commands/InstallationCommand.php | Modificato | Aggiunta configurazione interattiva DB |
Questa patch release corregge un bug nel processo di installazione che impediva la corretta sostituzione delle costanti nel file di configurazione quando queste utilizzavano apici doppi invece di apici singoli.
Corrette le espressioni regolari in InstallationManager.php per supportare sia apici singoli (') che apici doppi (") nella sostituzione delle costanti:
/(const\s+PROJECT\s*=\s*')[^']*(')//(const\s+PROJECT\s*=\s*)(['\"])[^'\"]*\\2/PROJECT, APPLICATION/(const\s+{$key}\s*=\s*')[^']*(')//(const\s+{$key}\s*=\s*)(['\"])[^'\"]*\\2/DATABASE_HOST, DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_PORT e tutte le altre costanti passate nel parametro $configScenario del bug:
Config/config.php del framework utilizza apici doppi per le stringhe (es. const PROJECT = "SismaFramework")InstallationManager cercavano solo apici singoliphp SismaFramework/Console/sisma install MyProject), le costanti non venivano sostituiteconfigFramework.php risultante manteneva i valori originali invece di quelli specificati dall'utente
Aggiornati i test per essere agnostici rispetto al tipo di apice utilizzato:
assertMatchesRegularExpression() invece di assertStringContainsString()testInstallWithDatabaseConfig(): Modificato per verificare la presenza delle costanti con entrambi i tipi di apicetestInstallWithForceOverwritesExistingConfig(): Modificato per usare regex nella verifica
Esempio di verifica aggiornata:
// ❌ Prima (verificava solo apici singoli):
$this->assertStringContainsString("const PROJECT = 'MyTestProject'", $content);
// ✅ Dopo (verifica entrambi i tipi di apici):
$this->assertMatchesRegularExpression("/const PROJECT = ['\"]MyTestProject['\"]/", $content);
Questa patch release corregge un bug nel file di configurazione del framework dove la costante LOG_PATH puntava a un percorso errato.
Corretto il percorso del file di log nella configurazione predefinita del framework:
const LOG_PATH = DIRECTORY_SEPARATOR . 'log.txt';const LOG_PATH = LOG_DIRECTORY_PATH . "log.txt";
Scenario del bug:
LOG_PATH era definita come DIRECTORY_SEPARATOR . 'log.txt'/log.txt su Linux, \log.txt su Windows)Sample/Logs/log.txt)
LOG_PATH utilizza correttamente LOG_DIRECTORY_PATH come base del percorso{ROOT}/Sample/Logs/log.txtLOG_PATH nel proprio file di configurazione non sono interessatiNuove Installazioni: Funzionano correttamente senza modifiche
Questa patch release corregge un bug nel processo di installazione che non copiava il file .htaccess necessario per il reindirizzamento verso la directory Public.
Aggiunta la copia del file .htaccess durante il processo di installazione automatica:
copyHtaccessFile() che copia il file .htaccess dalla directory del framework alla root del progetto--force: se il file esiste e non viene specificato --force, il file non viene sovrascrittoinstall() dopo copyPublicFolder().htaccess tra i file creatiScenario del bug:
php SismaFramework/Console/sisma install MyProject.htaccess non veniva copiato nella root del progettoPublic/index.php
.htaccess viene copiato automaticamente nella root del progettoPublic/ già configurateContenuto del file .htaccess:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} fbclid= [NC]
RewriteRule ^(.*)$ /$1? [R=301,L]
RewriteCond %{THE_REQUEST} \ /(.+/)?index\.php\/(.*)$ [NC]
RewriteRule ^(.+/)?index\.php\/(.*)$ /$1$2 [R=301,L]
RewriteCond %{THE_REQUEST} \ /(.+/)?index\.php(.*)$ [NC]
RewriteRule ^(.+/)?index\.php(.*)$ /$1$2 [R=301,L]
RewriteCond %{REQUEST_URI} !^/Public/
RewriteRule ^(.*)$ Public/ [L]
</IfModule>
.htaccess dal framework o configurare il virtual host per puntare alla directory PublicNuove Installazioni: Il file .htaccess viene copiato automaticamente
Questa patch release corregge tre bug critici nel processo di installazione automatica del framework che causavano errori nella generazione del file composer.json e nel riferimento al file di configurazione.
Corretto il formato del nome del progetto nel file composer.json generato durante l'installazione:
'name' => strtolower(str_replace(' ', '-', $projectName)) generava solo "nome-progetto"'name' => "vendor/{$normalizedName}" genera correttamente "vendor/nome-progetto"vendor/package-nameScenario del bug:
php SismaFramework/Console/sisma install MyProjectcomposer.json veniva creato con "name": "myproject" invece di "name": "vendor/myproject"composer install o composer update perché il formato non era valido
composer.json ha il formato nome corretto: "vendor/myproject"composer install funziona senza errori
Corretto il percorso del file di configurazione nel file Public/index.php generato durante l'installazione:
'SismaFramework' . DIRECTORY_SEPARATOR . 'Config''Config' (root del progetto), solo Autoload viene modificatoroot/Config/configFramework.php e non in root/SismaFramework/Config/configFramework.phpScenario del bug:
Config/configFramework.php viene creato nella root del progettoPublic/index.php veniva modificato per cercare il config in SismaFramework/Config/configFramework.php
Public/index.php include correttamente dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Config' . DIRECTORY_SEPARATOR . 'configFramework.php'SismaFramework/AutoloadCorretto il numero di livelli di risalita della costante ROOT_PATH nel file di configurazione installato:
__DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_UP . DIRECTORY_SEPARATOR . DIRECTORY_UP → __DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_UPScenario del bug:
SismaFramework/Config/config.php si trova in root/SismaFramework/Config/config.phpSismaFramework/Config → SismaFramework → root (corretto per il framework)Config/configFramework.php si trova in root/Config/configFramework.phpConfig → root → parent (errato)Config → root (corretto)
Aggiornati i test per riflettere le correzioni apportate:
'SismaFramework' . DIRECTORY_SEPARATOR . 'Config'
- ✅
Dopo: Verifica presenza di'Config' . DIRECTORY_SEPARATOR . 'configFramework.php'
'mytestproject'
- ✅
Dopo: Verifica nome come'vendor/mytestproject'
composer.json per aggiungere il prefisso vendor/
2. Verificare che Public/index.php punti a Config/configFramework.php e non a SismaFramework/Config/configFramework.php
3. Verificare che ROOT_PATH in Config/configFramework.php salga di un solo livello
Questa major release introduce miglioramenti architetturali significativi: rifattorizzazione completa di BaseForm con principi SOLID, semplificazione API Response attraverso rimozione del metodo pubblico setResponseType(), e implementazione completa dello standard PSR-3 per il logging con supporto per logger di terze parti.
La release introduce breaking changes: il metodo astratto customFilter() di BaseForm ora ritorna bool invece di void, il metodo pubblico setResponseType() di Response è stato rimosso in favore dell'immutabilità tramite constructor injection, e le classi ErrorHandler e Debugger sono state trasformate da statiche a di istanza per migliorare testabilità e dependency injection.
BaseForm è stata completamente rifattorizzata applicando il Single Responsibility Principle, con estrazione delle responsabilità in classi dedicate:
FilterManager (Core/BaseClasses/BaseForm/FilterManager.php): Gestisce la configurazione e l'applicazione dei filtri di validazioneaddFilterFieldMode(): Registra la configurazione del filtro per una proprietàhasFilter(): Verifica se una proprietà ha un filtro configuratogetFilterConfig(): Recupera la configurazione completa di un filtroapplyFilter(): Applica il filtro a un valore specificoisNullable(): Verifica se una proprietà accetta valori nullgetAllFilteredPropertyNames(): Ritorna l'elenco di tutte le proprietà con filtri configuratiFormValidator (Core/BaseClasses/BaseForm/FormValidator.php): Responsabile della validazione completa del formentityData (StandardEntity validato) e filterResult (bool)DataMapper, FilterManager e ConfigEntityResolver (Core/BaseClasses/BaseForm/EntityResolver.php): Gestisce la risoluzione e il popolamento delle entità a partire dai dati validatiresolveEntity(): Popola l'entità con i dati validati dal formVantaggi della rifattorizzazione:
BaseForm da oltre 400 linee a circa 200BaseForm ora accetta le nuove classi helper come parametri opzionali:
public function __construct(
?BaseEntity $baseEntity = null,
DataMapper $dataMapper = new DataMapper(),
FilterManager $filterManager = new FilterManager(),
?FormValidator $formValidator = null,
EntityResolver $entityResolver = new EntityResolver()
)
Questo permette di iniettare implementazioni custom per testing o estensioni.
SismaLogger (Core/HelperClasses/SismaLogger.php): Nuova classe che implementa Psr\Log\LoggerInterfaceemergency(), alert(), critical(), error(), warning(), notice(), info(), debug()log() con supporto completo per interpolazione dei placeholder ({key}) secondo PSR-3code, file, line, traceLocker e Config per massima testabilitàSismaLogReader (Core/HelperClasses/SismaLogReader.php): Nuova classe per la lettura strutturata dei logLogReaderInterface per permettere implementazioni customgetLogRowByRow() per lettura riga per riga dei file di logDebugger per visualizzazione nella debug barLogReaderInterface (Core/Interfaces/Logging/LogReaderInterface.php): Nuova interfaccia per astrazione lettori di log
ShouldBeLoggedException (Security/Interfaces/Exceptions/ShouldBeLoggedException.php): Nuova marker interfaceErrorHandler verifica automaticamente se un'eccezione implementa questa interfacciaVantaggi dell'implementazione PSR-3:
SismaLogger con qualsiasi logger PSR-3 compatibile (Monolog, Log4php, etc.)ErrorHandler, BaseAdapter, e altre classiContext-aware: Supporto per metadati contestuali tramite array $contextTesting facilitato: Possibilità di iniettare logger mock nei testCompatibilità framework: Integrazione semplificata con framework di terze parti
Esempio di utilizzo con logger custom:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Logger di terze parti (Monolog)
$monolog = new Logger('app');
$monolog->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));
// Injection in ErrorHandler
$errorHandler = new ErrorHandler(logger: $monolog);
$errorHandler->registerNonThrowableErrorHandler();
ErrorHandler e Debugger sono state completamente rifattorizzate da classi con metodi statici a classi di istanza con dependency injection:
ErrorHandler (Core/HelperClasses/ErrorHandler.php):ErrorHandler::disableErrorDisplay(), ErrorHandler::handleBaseException()LoggerInterface e ConfighandleNonThrowableError() → registerNonThrowableErrorHandler()handleBaseException() che verifica ShouldBeLoggedException prima di loggarehandleThrowableError() per gestione generica di ThrowableDebugger (Core/HelperClasses/Debugger.php):LogReaderInterfaceSismaLogReader (o implementazioni custom)Public/index.php: Modificato il bootstrap dell'applicazione per utilizzare le nuove classi di istanza:
// Prima (10.x):
ErrorHandler::disableErrorDisplay();
ErrorHandler::handleNonThrowableError();
Debugger::startExecutionTimeCalculation();
// Dopo (11.0.0):
$errorHandler = new ErrorHandler();
$errorHandler->disableErrorDisplay();
$errorHandler->registerNonThrowableErrorHandler();
$debugger = new Debugger();
$debugger->startExecutionTimeCalculation();
$dispatcher = new Dispatcher(debugger: $debugger);
Motivazione della trasformazione:
BaseAdapter e le sue sottoclassi (AdapterMysql) ora accettano LoggerInterface via costruttore per logging delle query SQL e degli errori di connessione database.
Render e Router sono state rifattorizzate implementando il pattern Facade combinato con Singleton, separando la logica di business in Service classes dedicate:
RenderService (Core/Services/RenderService.php): Nuova classe singleton che contiene tutta la logica di rendering delle viewgetInstance(), setInstance(), resetInstance()generateView(), generateData(), generateJson()Localizator, Debugger, Config per massima testabilitàRouterService (Core/Services/RouterService.php): Nuova classe singleton che gestisce tutte le operazioni di routinggetInstance(), setInstance(), resetInstance()redirect(), concatenateMetaUrl(), setMetaUrl(), getMetaUrl()setActualCleanUrl(), getControllerUrl(), getActionUrl(), getActualCleanUrl()getRootUrl(), getActualUrl(), resetMetaUrl(), reloadWithParsedQueryString()Render come Facade (Core/HelperClasses/Render.php): La classe Render è ora una facade pura che delega a RenderService__callStatic() per chiamate statiche: Render::generateView() → RenderService::getInstance()->generateView()__call() per chiamate di istanza: $render->generateView() → RenderService::getInstance()->generateView()Router come Facade (Core/HelperClasses/Router.php): La classe Router è ora una facade pura che delega a RouterService__callStatic() per chiamate statiche: Router::redirect() → RouterService::getInstance()->redirect()__call() per chiamate di istanza: $router->redirect() → RouterService::getInstance()->redirect()Vantaggi del Pattern Facade + Singleton:
Render::generateView()) che di istanza ($this->render->generateView())BaseController ora ha proprietà $this->render e $this->router utilizzabili come istanzeTestabilità: Metodi setInstance() permettono di iniettare mock nei testIsolamento dello stato: Lo stato è incapsulato nel singleton invece di proprietà statiche sparseRetrocompatibilità al 100%: Nessun breaking change, tutto il codice esistente continua a funzionareSeparazione delle responsabilità: Facade (interfaccia pubblica) separato da Service (logica di business)Facilità di testing: Metodo resetInstance() permette di resettare lo stato nei test
Utilizzo nei Controller:
class ProductController extends BaseController
{
public function index(): Response
{
// Sintassi di istanza (nuovo, preferito):
return $this->render->generateView('product/index', $this->vars);
// Sintassi statica (legacy, ancora supportato):
return Render::generateView('product/index', $this->vars);
}
public function create(): Response
{
// Sintassi di istanza per router:
return $this->router->redirect('product/list');
// Sintassi statica (legacy):
return Router::redirect('product/list');
}
}
BaseController ora inizializza le proprietà $this->render e $this->router nel costruttore:
protected RenderService $router;
protected RenderService $render;
public function __construct(DataMapper $dataMapper = new DataMapper(), Debugger $debugger = new Debugger())
{
$this->dataMapper = $dataMapper;
$this->debugger = $debugger;
$this->router = RouterService::getInstance();
$this->render = RenderService::getInstance();
// ...
}
Questo permette di utilizzare $this->render e $this->router come istanze in tutti i controller che estendono BaseController.
Prima (10.x):
$response = new Response();
$response->setResponseType(ResponseType::httpNotFound);
Dopo (11.0.0):
$response = new Response(ResponseType::httpNotFound);
Motivazione:
$responseType ora readonly (PHP 8.1+) per garantire immutabilità a livello di linguaggioImpatto: Il metodo setResponseType() non è più disponibile. Utilizzare il costruttore per impostare il response type.
Azione richiesta:
customFilter(): Il metodo astratto customFilter() ora ritorna bool invece di void
Prima (10.x):
abstract protected function customFilter(): void;
Dopo (11.0.0):
abstract protected function customFilter(): bool;
Motivazione: Il nuovo tipo di ritorno
bool permette al metodo customFilter() di contribuire al risultato finale di validazione del form. Ritornando true se la validazione custom ha successo o false in caso di errori, si ottiene un'API più coerente e un flusso di validazione più chiaro.
Impatto: Tutte le classi che estendono
BaseForm devono essere aggiornate per ritornare un valore booleano dal metodo customFilter().
Azione richiesta:
return true; alla fine del metodo customFilter() se non ci sono errori di validazione customfalse quando la validazione custom fallisce // Prima (10.x):
protected function customFilter(): void
{
if ($this->entity->startDate > $this->entity->endDate) {
$this->formFilterError->startDateError = true;
}
}
// Dopo (11.0.0):
protected function customFilter(): bool
{
if ($this->entity->startDate > $this->entity->endDate) {
$this->formFilterError->startDateError = true;
return false;
}
return true;
}
ErrorHandler e Debugger non sono più utilizzabili con chiamate statiche. È necessario creare istanze di queste classi.
Prima (10.x):
ErrorHandler::disableErrorDisplay();
ErrorHandler::handleNonThrowableError();
Debugger::startExecutionTimeCalculation();
Dopo (11.0.0):
$errorHandler = new ErrorHandler();
$errorHandler->disableErrorDisplay();
$errorHandler->registerNonThrowableErrorHandler();
$debugger = new Debugger();
$debugger->startExecutionTimeCalculation();
Motivazione:
Impatto: Il file
Public/index.php deve essere aggiornato per creare istanze delle classi invece di usare metodi statici. Tutte le chiamate statiche a ErrorHandler e Debugger devono essere convertite a chiamate di istanza.
Azione richiesta:
Public/index.php per creare istanze di ErrorHandler e DebuggerErrorHandlerEncryptor::createInitializationVector(): Il metodo createInizializationVector() è stato rinominato in createInitializationVector() per correggere l'errore di spelling.
Prima (10.x):
$iv = Encryptor::createInizializationVector();
Dopo (11.0.0):
$iv = Encryptor::createInitializationVector();
Motivazione: Correzione di un typo nel nome del metodo per migliorare la coerenza del codebase e facilitare l'uso dell'API.
Impatto: Questo è un potenziale breaking change se il metodo veniva chiamato direttamente nel codice utente. Il metodo è principalmente utilizzato internamente dal framework (in
DataMapper per proprietà crittografate), ma potrebbe essere stato usato in codice custom per crittografia manuale.
Azione richiesta:
createInizializationVector nel proprio codebasecreateInitializationVector (con la "t" invece della "z")BaseForm ora includono messaggi descrittivi che spiegano chiaramente il problema:FormException: "Entity name returned by getEntityName() must be a subclass of BaseEntity"InvalidArgumentException: "BaseEntity parameter must be an instance of {EntityClassName} or null"Questo facilita il debugging e rende più chiaro agli sviluppatori il motivo degli errori di configurazione.
customFilter():BaseFormTest.php: Aggiornato per testare il valore di ritorno booleanoFormWithCustomFilterFalse.php per verificare il comportamento quando customFilter() ritorna falseEntityResolver, FilterManager, FormValidator) sono completamente testate attraverso i test esistenti di BaseForm, garantendo che la rifattorizzazione non abbia introdotto regressioni.
LoggerTest.php: Test completi per la classe SismaLoggeremergency(), alert(), critical(), error(), warning(), notice(), info(), debug(){key})code, file, line, trace)ErrorHandlerTest.php: Test completi per la nuova classe ErrorHandler di istanzaShouldBeLoggedExceptionNoLogException) non vengano scritte nel logLoggerInterface customBaseException e Throwable genericiTestLoggableException.php: Fixture di test per eccezione che implementa ShouldBeLoggedException
TestNonLoggableException.php: Fixture di test per eccezione che non implementa l'interfaccia
DebuggerTest.php: Modificato per creare istanze di Debugger con dependency injection di LogReaderInterfaceRenderServiceTest.php (259 linee): Test completi per la classe RenderServicegetInstance() ritorni sempre la stessa istanzasetInstance() e resetInstance() per dependency injection nei testgenerateView(), generateData(), generateJson() con vari parametriLocalizator, Debugger, ConfigRouterServiceTest.php (195 linee): Test completi per la classe RouterServicegetInstance(), setInstance(), resetInstance()redirect() con vari scenari di URLconcatenateMetaUrl(), setMetaUrl(), getMetaUrl()setActualCleanUrl(), getControllerUrl(), getActionUrl(), getActualCleanUrl()getRootUrl(), getActualUrl() con diverse configurazioni di serverresetMetaUrl() per reset dello statoreloadWithParsedQueryString() per parsing query string in URLBaseControllerTest.php: Esteso con nuovi test per le proprietà $render e $router$this->render sia istanza di RenderService$this->router sia istanza di RouterServiceBaseControllercontrollerUrl, actionUrl, metaUrl, etc.) siano popolate correttamenteCopertura totale: +603 linee di test per garantire affidabilità del nuovo pattern architetturale.
@internal per indicare che fanno parte dell'implementazione interna di BaseForm e non dovrebbero essere utilizzate direttamente dagli sviluppatori.
Questa è una major release (11.0.0) che introduce breaking changes. L'aggiornamento richiede modifiche al codice esistente:
BaseForm devono aggiornare il metodo customFilter() per ritornare boolPublic/index.php deve essere aggiornato per creare istanze di ErrorHandler e Debugger invece di usare metodi staticiBaseForm mantengono la stessa interfaccianew Response()$this->render->generateView() e $this->router->redirect() invece della sintassi statica legacy
: bool alla firma del metodo customFilter()true quando la validazione custom ha successofalse quando la validazione custom falliscePublic/index.php per creare istanze invece di chiamare metodi staticiErrorHandler::disableErrorDisplay() con $errorHandler = new ErrorHandler(); $errorHandler->disableErrorDisplay();ErrorHandler::handleNonThrowableError() con $errorHandler->registerNonThrowableErrorHandler();Debugger::startExecutionTimeCalculation() con $debugger = new Debugger(); $debugger->startExecutionTimeCalculation();$debugger nel costruttore di Dispatcher: new Dispatcher(debugger: $debugger)ErrorHandler::handleBaseException() con $errorHandler->handleBaseException()ErrorHandler::handleThrowableError() con $errorHandler->handleThrowableError()
Questa patch release corregge un bug critico introdotto nella versione 10.1.0 nel metodo buildPropertiesConditions di DependentModel e SelfReferencedModel, che impediva il corretto override del metodo di BaseModel. Inoltre corregge errori sistematici nella test suite che utilizzavano nomi di proprietà in formato snake_case invece di camelCase.
Corretto un errore di battitura nel nome del metodo introdotto nella versione 10.1.0 che impediva l'override corretto del metodo di BaseModel:
protected function buildPropertyConditions(...) (singolare - typo)protected function buildPropertiesConditions(...) (plurale - corretto)BaseModelScenario del bug:
BaseModel si chiamava correttamente buildPropertiesConditions (plurale)DependentModel e SelfReferencedModel era stato erroneamente chiamato buildPropertyConditions (singolare)
appendCondition() non veniva passato correttamente per distinguere proprietà entity da builtinDataType::typeEntity invece di essere determinati dinamicamenteReferencedEntity, il quarto parametro ($isForeignKey) non veniva impostato a true_id ai nomi delle colonne foreign keytypeEntity invece del tipo correttoImpatto della correzione:
DependentModel/SelfReferencedModelappendCondition() viene passato correttamente: $propertyValue instanceof ReferencedEntityDataType::fromReflection() invece di essere hardcodati_id per le foreign keyuse SismaFramework\Orm\ExtendedClasses\ReferencedEntity; in DependentModel.php$propertyValue instanceof ReferencedEntityCorretti errori sistematici nella test suite che utilizzavano nomi di proprietà in formato snake_case (convenzione database) invece di camelCase (convenzione PHP):
'referenced_entity_with_initialization', 'string_with_inizialization', 'nullable_string_with_inizialization''referencedEntityWithInitialization', 'stringWithInizialization', 'nullableStringWithInizialization'
'parent_self_referenced_sample', 'base_sample''parentSelfReferencedSample', 'baseSample'
Motivo del problema:
getEntityCollectionByEntity(), countEntityCollectionByEntity(), deleteEntityCollectionByEntity() accettano array con chiavi = ReflectionException perché new \ReflectionProperty($entityName, 'referenced_entity_with_initialization') cercava una proprietà inesistente$referencedEntityWithInitialization (camelCase)Esempio di correzione:
// ❌ PRIMA (errato - nome colonna database):
$posts = $postModel->getEntityCollectionByEntity(['author_id' => $user]);
// ✅ DOPO (corretto - nome proprietà PHP):
$posts = $postModel->getEntityCollectionByEntity(['author' => $user]);
Convenzione del framework:
getByAuthor($user) → genera internamente ['author' => $user] (camelCase)['author' => $user] (camelCase), non ['author_id' => $user]
Aggiunti 2 nuovi test in DependentModelTest.php che avrebbero catturato il bug della versione 10.1.0:
testBuildPropertiesConditionsPassesCorrectFourthParameterToAppendCondition():appendCondition() sia true per proprietà ReferencedEntityfalse per proprietà builtin (string, bool, int)testBuildPropertiesConditionsGeneratesCorrectBindTypesForMixedProperties():$bindTypes passati a DataMapper::getCount()DataType::typeEntity per ReferencedEntity, DataType::typeBoolean per bool, DataType::typeString per stringDataType::typeEntity per tutte le proprietà
Copertura test totale:
_id per foreign keyType Safety: Bind types corretti per tutte le tipologie di proprietàStabilità: Eliminati errori di binding e query malformateTest Coverage: Aggiunti test specifici per prevenire regressioni futureQualità: Test suite conforme alle convenzioni del framework
Quando si utilizzano i metodi getEntityCollectionByEntity(), countEntityCollectionByEntity(), deleteEntityCollectionByEntity(), ricordare che le chiavi dell'array devono essere
// ✅ CORRETTO:
$posts = $postModel->getEntityCollectionByEntity([
'author' => $user, // nome proprietà PHP
'category' => $category, // nome proprietà PHP
'isPublished' => true // nome proprietà PHP
]);
// ❌ ERRATO:
$posts = $postModel->getEntityCollectionByEntity([
'author_id' => $user, // nome colonna database - causerà ReflectionException
'category_id' => $category, // nome colonna database - causerà ReflectionException
'is_published' => true // nome colonna database - causerà ReflectionException
]);
La conversione da camelCase (proprietà PHP) a snake_case (colonne database) avviene automaticamente all'interno del framework tramite NotationManager.
Questa patch release corregge un bug critico introdotto nella versione 10.1.5 relativo alla definizione ricorsiva della costante LOG_DIRECTORY_PATH nel file di configurazione.
Corretto un bug introdotto nella versione 10.1.5 che causava una definizione ricorsiva della costante LOG_DIRECTORY_PATH:
const LOG_DIRECTORY_PATH = SYSTEM_PATH . APPLICATION_PATH . LOGS . LOG_DIRECTORY_PATH;const LOG_DIRECTORY_PATH = SYSTEM_PATH . APPLICATION_PATH . LOGS . DIRECTORY_SEPARATOR;DIRECTORY_SEPARATOR invece di fare riferimento a se stessacopyConfigFolder() per riflettere la definizione corretta: // Pattern di sostituzione corretto (linee 115-116):
"const LOG_DIRECTORY_PATH = SYSTEM_PATH . APPLICATION_PATH . LOGS . DIRECTORY_SEPARATOR;",
"const LOG_DIRECTORY_PATH = ROOT_PATH . LOGS . DIRECTORY_SEPARATOR;",
testInstallCopiesConfigFile() per verificare la costante corretta: $this->assertStringContainsString(
"const LOG_DIRECTORY_PATH = ROOT_PATH . LOGS . DIRECTORY_SEPARATOR;",
$content
);
Scenario del bug:
LOG_DIRECTORY_PATH era definita usando se stessa: ... . LOG_DIRECTORY_PATH
DIRECTORY_SEPARATORConfig/config.php del framework che il processo di installazione automatica utilizzano la definizione correttaConfig/configFramework.php sostituendo la riga errata
LOG_DIRECTORY_PATHStabilità: Prevenuti potenziali errori a runtime causati dalla definizione errataQualità: Test aggiornati per garantire che il processo di installazione generi la costante corretta
Questa patch release corregge un problema nel processo di installazione automatica che non modificava correttamente alcune costanti del file di configurazione framework nella root del progetto.
Corretto il processo di installazione per aggiornare automaticamente le costanti del file configFramework.php creato nella root del progetto durante l'installazione:
copyConfigFolder() ora modifica correttamente le seguenti costanti quando crea il file di configurazione nella root del progetto:1. APPLICATION:
'Sample' (valore originale del framework)'Application'
2. REFERENCE_CACHE_DIRECTORY:
SYSTEM_PATH . APPLICATION_PATH . CACHE . DIRECTORY_SEPARATORROOT_PATH . CACHE . DIRECTORY_SEPARATOR
3. LOG_DIRECTORY_PATH:
SYSTEM_PATH . APPLICATION_PATH . LOGS . LOG_DIRECTORY_PATHROOT_PATH . LOGS . LOG_DIRECTORY_PATH
4. MODULE_FOLDERS:
'SismaFramework'[]
Scenario del problema:
php SismaFramework/Console/sisma install MyProjectConfig/configFramework.php veniva creato nella root del progettoPROJECT veniva aggiornata correttamente, ma APPLICATION, REFERENCE_CACHE_DIRECTORY, LOG_DIRECTORY_PATH e MODULE_FOLDERS mantenevano i valori del framework originale
configFramework.php ha i valori corretti per un nuovo progettoAggiornati i test per verificare le nuove modifiche al processo di installazione:
const APPLICATION = 'Application'
- Verifica const REFERENCE_CACHE_DIRECTORY = ROOT_PATH . CACHE . DIRECTORY_SEPARATOR;
- Verifica const LOG_DIRECTORY_PATH = ROOT_PATH . LOGS . LOG_DIRECTORY_PATH;
- Verifica const MODULE_FOLDERS = [];
config.php di test più completo con tutte le costanti necessarie: - Aggiunge costanti APPLICATION = 'Sample'
- Aggiunge costanti CACHE, LOGS, SYSTEM_PATH, APPLICATION_PATH, ROOT_PATH
- Aggiunge REFERENCE_CACHE_DIRECTORY e LOG_DIRECTORY_PATH con valori originali del framework
- Aggiunge MODULE_FOLDERS con 'SismaFramework' nel array
- Questo permette ai test di verificare che la trasformazione avvenga correttamente
SismaFramework/Config/config.php originale rimane inalteratoInstallazioni Esistenti: Progetti già installati non sono influenzati, solo nuove installazioni beneficiano della correzione
Questa patch release corregge un bug nel processo di installazione automatica e aggiorna la test suite per conformità alle best practice di PHPUnit 11+ eliminando deprecation notices relative all'uso di mock al posto di stub.
Corretto il processo di installazione automatica per rinominare correttamente il riferimento al file di configurazione in Public/index.php:
Public/index.php copiato manteneva il riferimento hardcoded a 'Config' . DIRECTORY_SEPARATOR . 'config.php''Config' . DIRECTORY_SEPARATOR . 'configFramework.php' $patterns = [
"dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Config'",
"dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Autoload'",
"'Config' . DIRECTORY_SEPARATOR . 'config.php'", // ← NUOVO
];
$replacements = [
"dirname(__DIR__) . DIRECTORY_SEPARATOR . 'SismaFramework' . DIRECTORY_SEPARATOR . 'Config'",
"dirname(__DIR__) . DIRECTORY_SEPARATOR . 'SismaFramework' . DIRECTORY_SEPARATOR . 'Autoload'",
"'Config' . DIRECTORY_SEPARATOR . 'configFramework.php'", // ← NUOVO
];
Scenario del bug:
php Console/sisma install MyProjectPublic/index.php veniva copiato e aggiornato per i path di Autoload e Configconfig.php invece di configFramework.php
Public/index.php include correttamente Config/configFramework.php
Aggiornati tutti i file di test per utilizzare createStub() al posto di createMock() quando non vengono configurate aspettative (expectations), eliminando deprecation notices introdotte in PHPUnit 11:
expects() su oggetti creati con createStub()createStub() per test stub (solo valori di ritorno), createMock() per mock object (verifica interazioni)InstallationCommandTest.php: $mockInstallationManager → $installationManagerStub
* ScaffoldCommandTest.php: $mockScaffoldingManager → $scaffoldingManagerStub
* ScaffoldingManagerTest.php: $configMock → $configStub (2 occorrenze)
* InstallationManagerTest.php: Rimossi setAccessible(true) deprecati (PHP 8.1+)
BaseFixtureTest.php: $configMock → $configStub, $dataMapperMock → stub
* BaseFormTest.php: $configMock → $configStub, $dataMapperMock → stub, $requestMock → stub
* FilterTypeTest.php, AutoloaderTest.php, ConfigTest.php, DebuggerTest.php, DispatcherTest.php
* EncryptorTest.php, FilterTest.php, FixturesManagerTest.php, LoggerTest.php
* ModuleManagerTest.php, NotationManagerTest.php
AdapterMysqlTest.php, BaseEntityTest.php, BaseModelTest.php, SismaCollectionTest.php
* DependentModelTest.php, ReferencedEntityTest.php, SelfReferencedEntityTest.php, SelfReferencedModelTest.php
* CacheTest.php, DataMapperTest.php, ErrorHandlerTest.php, ProcessedEntitiesCollectionTest.php, QueryTest.php
* JoinEagerLoadingTest.php, ResultSetMysqlTest.php
BasePermissionTest.php, BaseVoterTest.php, AuthenticationTest.php
// ❌ Prima (PHPUnit 11 deprecation warning):
$configMock = $this->createMock(Config::class);
$configMock->expects($this->any()) // ← expects() su stub non necessario
->method('__get')
->willReturnMap([...]);
// ✅ Dopo (conforme PHPUnit 11+):
$configStub = $this->createStub(Config::class);
$configStub->method('__get') // ← solo configurazione valori di ritorno
->willReturnMap([...]);
setAccessible(true) in InstallationManagerTest.php (non più necessari per proprietà private da PHP 8.1)
Refactorizzata formattazione del codice in InstallationManager.php per migliorare leggibilità:
Impatto: Nessun cambiamento funzionale, solo miglioramento della manutenibilità.
Questa patch release corregge un bug critico nel sistema di parsing degli argomenti posizionali dei comandi CLI che impediva il corretto funzionamento del comando install.
Corretto bug nel CommandDispatcher che utilizzava nomi hardcodati per gli argomenti posizionali, causando incompatibilità tra comandi diversi:
entity, module)0, 1, 2, ...) per massima flessibilitàgetArgument('0') invece di getArgument('projectName')getArgument('0') e getArgument('1') invece di entity e moduleScenario del bug:
php Console/sisma install MyProjectCommandDispatcher assegnava l'argomento come ['entity' => 'MyProject']InstallationCommand cercava getArgument('projectName') → null
CommandDispatcher assegna: ['0' => 'MyProject']InstallationCommand legge getArgument('0') → 'MyProject'
testSuccessfulInstallation(): ['0' => 'MyProject'] invece di ['projectName' => 'MyProject']testInstallationWithDatabaseOptions(): Stessa modificatestInstallationFailure(): Stessa modificatestExecuteWithMissingModule(): ['0' => 'User'] invece di ['entity' => 'User']testSuccessfulExecution(): ['0' => 'MockEntity', '1' => 'TestModule']php Console/sisma install MyProject continua a funzionarephp Console/sisma scaffold User Blog continua a funzionareinstall ora funziona come previstoQuesta patch release migliora la robustezza della gestione dei path nel router attraverso la normalizzazione automatica degli slash, eliminando potenziali bug da doppi slash o slash mancanti.
Risolti warning di analisi statica segnalati da PHPStan per migliorare la qualità e la correttezza del codice:
return non necessario dai costruttori delle eccezioniApplicationPathException.php:41, EntityPathException.php:47, ModulePathException.php:41returnreturn parent::__construct($message, $code, $previous);parent::__construct($message, $code, $previous);$this->checkDependencies($this->entityReflection)$this->checkDependencies()findWithJoins() (codice non utilizzato da refactoring precedente)Authenticationuse SismaFramework\Security\Authentication;use SismaFramework\Security\HttpClasses\Authentication;SampleReferencedEntityMigliorata la gestione dei path nel Router per rendere più robusta e consistente la concatenazione degli URL:
concatenateMetaUrl(): Il metodo ora gestisce automaticamente l'aggiunta del / iniziale e rimuove eventuali trailing slash tramite rtrim()/ era del chiamante (concatenateMetaUrl('/path'))
- ✅ Dopo: Il metodo normalizza automaticamente il path (concatenateMetaUrl('path') → /path)
redirect(): Aggiunto rtrim($relativeUrl, '/') per normalizzare l'URL di destinazione prima del redirectrtrim() rende l'operazione sempre sicura
- Prevenzione doppi slash: eliminati potenziali path malformati come /meta//url
- API più intuitiva: non serve più passare / manualmente
slicePathElement(): Aggiornata la chiamata a concatenateMetaUrl() per passare il path senza / inizialeRouter::concatenateMetaUrl('/' . $this->pathController)
- Dopo: Router::concatenateMetaUrl($this->pathController)
concatenateMetaUrl()testGetActualUrl(): Ora utilizza chiamate separate (concatenateMetaUrl('meta') + concatenateMetaUrl('url')) invece di una singola chiamata con path completotestSetMetaUrlOverwritesPreviousValue(): Stessa modifica per testare la sovrascritturartrim(), sia concatenateMetaUrl('/path') che concatenateMetaUrl('path') producono lo stesso risultatoQuesta patch release corregge un bug critico di conformità agli standard HTTP che impediva la riproduzione di video in Safari. Implementato il supporto completo per HTTP Range Requests (RFC 7233) con gestione di 206 Partial Content e 416 Range Not Satisfiable. Migliorata l'API della classe Response con constructor injection.
Corretto bug critico nel serving di file statici che causava la mancata riproduzione di video in Safari:
Range e restituiva sempre 200 OK con l'intero fileviewResource() e downloadResource(): Rilevamento header Range e delega a servePartialContent()servePartialContent(): Gestisce risposta 206 Partial ContentparseRangeHeader(): Validazione formato con regex e controlligetResourceDataRange(): Lettura efficiente chunk-based (8KB)Content-Range: bytes */filesize settato automaticamenteScenario del bug:
Range: bytes=0-1023
Migliorata l'API della classe Response:
?ResponseType $responseType = nullnew Response(ResponseType::httpPartialContent)ResourceMaker::servePartialContent(): -2 lineeRender::getResponse(): Metodo rimosso (-7 linee)Render: Return diretto in generateView(), generateData(), generateJson()Range, Content-Range, Accept-Ranges, Content-LengthBenvenuti alla release 10.1.0, una delle più ricche di novità nella storia del framework! Utility CLI rivoluzionano il flusso di sviluppo quotidiano, con scaffolding automatico e installazione guidata che accelerano drasticamente la creazione di nuovi progetti. Ottimizzato profondamente il Dispatcher attraverso una rifatorizzazione completa seguendo i principi SOLID, separando le responsabilità in sette classi specializzate che rendono il codice più manutenibile e testabile.
Nascono nuove funzionalità per l'ORM: le funzioni di aggregazione SQL (AVG, MAX, MIN, SUM) permettono ora query analitiche avanzate con supporto per DISTINCT, alias, subquery e aggregazioni multiple, mentre l'estensione del sistema di query dinamiche con metaprogrammazione a tutte le proprietà (non più solo entità referenziate) riduce drasticamente la necessità di scrivere metodi repository ripetitivi generando automaticamente query type-safe, e il supporto completo per JOIN SQL con eager loading gerarchico multi-entità risolve definitivamente il problema N+1 delle query supportando relazioni nested a più livelli con dot notation e sintassi array.
Comandi CLI di scaffolding generano automaticamente l'intero stack CRUD (Controller, Model, Form, Views) a partire da un'Entity esistente, mentre il sistema di installazione configura progetti completi in pochi secondi. Oltre 400 linee di nuovi test garantiscono una copertura completa di tutte le nuove funzionalità, assicurando robustezza e affidabilità.
Molto è stato fatto anche sul fronte architetturale: la rifatorizzazione del Dispatcher riduce la complessità ciclomatica da oltre 400 linee a meno di 200, creando sette nuovi file di helper classes che gestiscono routing, factory dei controller, parsing degli argomenti e gestione delle risorse statiche. Pattern consolidati come Command/Manager vengono applicati sistematicamente ai comandi CLI, con dependency injection e gestione centralizzata delle eccezioni.
Livello enterprise raggiunto con le funzioni di aggregazione ORM: AVG, MAX, MIN e SUM supportano ora DISTINCT, alias personalizzati, subquery e aggregazioni multiple sulla stessa query, portando l'ORM a competere con i framework più evoluti. Estesa significativamente anche la documentazione, con due nuove guide complete per scaffolding e installazione automatica.
Architettura completamente rinnovata: il Dispatcher è stato suddiviso in sette classi specializzate (RouteResolver, ControllerFactory, ActionArgumentsParser, ResourceHandler, RouteInfo, FixturesManager, ResourceMaker) che seguono i principi SOLID e facilitano manutenibilità ed estensibilità future.
Notevole il numero di miglioramenti introdotti in questa release, che rappresenta un punto di svolta nella maturità del framework. Dedichiamo questa versione a tutti gli sviluppatori che quotidianamente utilizzano SismaFramework per creare applicazioni robuste e scalabili.
Nuove possibilità si aprono con questi strumenti professionali: template personalizzabili per lo scaffolding, configurazione database da CLI, protezione contro sovrascritture accidentali e auto-detection intelligente del tipo di Model più appropriato.
Ottima base per futuri sviluppi: questa release pone le fondamenta per ulteriori miglioramenti al sistema di scaffolding e all'ORM, con possibilità di estensione illimitate che verranno esplorate nelle prossime versioni.
Finalmente, dopo mesi di lavoro intenso, possiamo dire che il framework ha raggiunto un livello di maturità che lo rende adatto anche a progetti complessi e mission-critical.
Ricordiamo che questa release è completamente retrocompatibile e l'aggiornamento è fortemente consigliato a tutti gli utenti della versione 10.0.x per beneficiare di questi importanti miglioramenti.
Articolata in tre aree principali (CLI Tools, Architettura, ORM), questa release rappresenta un passo significativo nell'evoluzione del framework, portando strumenti professionali di livello enterprise alla portata di tutti gli sviluppatori PHP.
scaffold che genera automaticamente Controller, Model, Form e Views a partire da un'Entity esistente.index, create, update, delete)BaseModel, DependentModel, o SelfReferencedModel)--template=PATH--forceEsempio di utilizzo:
php SismaFramework/Console/sisma scaffold Product Catalog
php SismaFramework/Console/sisma scaffold User Blog --type=DependentModel --force
install per configurare rapidamente nuovi progetti.Config/, Public/, Cache/, Logs/, filesystemMedia/)configFramework.php con il nome del progettoPublic/index.php per puntare alla sottocartella SismaFramework/Esempio di utilizzo:
php SismaFramework/Console/sisma install MyProject
php SismaFramework/Console/sisma install BlogPersonale --db-host=localhost --db-name=blog_db --db-user=root --db-pass=secret
getByReferencedEntity(), ora supporta qualsiasi proprietà: getByName(), countByStatus(), deleteByEmail()int, string, float, bool), oggetti custom (SismaDate, SismaDateTime, SismaTime), enum PHP 8.1+, proprietà nullableReflectionType::allowsNull() per gestione corretta dei valori nullgetByNameAndCategory(), countByStatusAndType()getEntityCollectionByEntity() rimangono funzionanti ma vengono marcati @deprecated (rimozione prevista in v11.0.0)buildPropertyConditions() che supporta sia entità che proprietà builtinEsempi di utilizzo:
// PRIMA (solo entità referenziate):
$model->getEntityCollectionByEntity(['referenced_entity' => $entity]);
// ADESSO (qualsiasi proprietà):
$users = $userModel->getByStatus(UserStatus::ACTIVE);
$count = $productModel->countByPrice(99.99);
$model->deleteByEmail('test@example.com');
// Proprietà multiple con AND logico:
$products = $productModel->getByNameAndCategory('iPhone', $electronics);
// Con searchKey e paginazione:
$articles = $articleModel->getByAuthor($author, 'keyword', ['date' => 'DESC'], 0, 20);
// Query gerarchiche (SelfReferencedModel):
$subCategories = $categoryModel->getByParentAndActive($parent, true);
// Valori null su proprietà nullable:
$orphans = $entityModel->getByNullableParent(null); // WHERE nullable_parent IS NULL
Dispatcher è stato completamente rifatorizzato seguendo i principi SOLID, con separazione delle responsabilità in classi dedicate:RouteResolver: Gestisce il parsing e la risoluzione delle route dall'URL, determinando modulo, controller e actionControllerFactory: Responsabile della creazione e istanziazione dei controller con dependency injection automaticaActionArgumentsParser: Analizza e prepara gli argomenti per i metodi action, gestendo type-hinting e auto-wiring delle entitàResourceHandler: Gestisce il serving dei file statici (CSS, JS, immagini) separando questa logica dal flusso principaleRouteInfo: Value object immutabile che contiene tutte le informazioni sulla route correnteFixturesManager: Estratta la logica di gestione delle fixtures in una classe dedicataResourceMaker: Gestisce la creazione e lo streaming ottimizzato delle risorse staticheVantaggi della rifatorizzazione:
ScaffoldCommand + ScaffoldingManagerInstallationCommand + InstallationManagersisma@deprecated dalla versione 11.0.0 in favore del sistema di query dinamiche:DependentModel: countEntityCollectionByEntity(), getEntityCollectionByEntity(), deleteEntityCollectionByEntity()SelfReferencedModel: countEntityCollectionByParentAndEntity(), getEntityCollectionByParentAndEntity(), deleteEntityCollectionByParentAndEntity()--forceNotDependentEntity::$nullableString, SelfReferencedSample::$nullableText) per testare correttamente la gestione dei valori nulldocs/scaffolding.md):docs/installation.md):Query: Introdotti i metodi setAVG(), setMax(), setMin(), e setSum() per applicare funzioni di aggregazione alle colonne$distinct per applicare l'aggregazione solo su valori distinti$append permette di aggiungere funzioni di aggregazione a colonne già selezionate, consentendo query con multiple aggregazioni$columnAliasQuery (subquery)AggregationFunction con i casi max e min (in aggiunta a avg, count, sum)opAggregationFunction() in BaseAdapter per gestire la generazione SQL delle funzioni aggregateEsempio di utilizzo:
// Media dei prezzi
$query->setAVG('price', 'average_price');
// Somma con DISTINCT
$query->setSum('amount', 'total', distinct: true);
// Multiple aggregazioni
$query->setMin('price', 'min_price')
->setMax('price', 'max_price', append: true)
->setAVG('price', 'avg_price', append: true);
JoinType: Introdotta enumerazione per gestire in modo type-safe i diversi tipi di JOIN (INNER, LEFT, RIGHT, CROSS)$joins e metodi appendJoin(), appendJoinOnForeignKey(), hasJoins(), getJoins(), getColumns() nella classe QuerygetEntityCollectionWithRelations(): Carica collection con relazioni via JOIN e batch loadinggetEntityByIdWithRelations(): Carica singola entità con le sue relazioniflattenRelations(): Normalizza sintassi dot notation e array nested per relazioni multi-livelloappendNestedRelationJoin(): Costruisce ricorsivamente JOIN per relazioni nested['author.country.continent']['author' => ['country' => ['continent']]]$joinMetadata per tracciare metadati delle tabelle joinedconvertToHierarchicalEntity(): Separa dati entità principali da nested entitieshydrateNestedEntities(): Idratazione ricorsiva di relazioni multi-livellogetEntityClassForAlias(): Risoluzione entity class da alias JOINbuildJoinedColumns(): Genera automaticamente colonne con alias (separatore __)buildJoinMetadata(): Costruisce metadati JOIN includendo relatedEntityClassbuildJoinOnForeignKey() per includere relatedEntityClass nei metadatiEsempi di utilizzo:
// Many-to-one: eager loading con JOIN
$articles = $articleModel->getEntityCollectionWithRelations(['author', 'category']);
foreach ($articles as $article) {
echo $article->author->name; // Già caricato, nessuna query N+1
}
// One-to-many: eager loading con batch loading
$authors = $authorModel->getEntityCollectionWithRelations(['articleCollection']);
// Relazioni nested multi-livello (dot notation)
$articles = $articleModel->getEntityCollectionWithRelations(['author.country.continent']);
// Relazioni nested (sintassi array)
$articles = $articleModel->getEntityCollectionWithRelations([
'author' => ['country', 'publisher' => ['city']]
]);
// SelfReferencedEntity (tree structures)
$categories = $categoryModel->getEntityCollectionWithRelations([
'parentCategory', // Padre
'sonCollection' // Figli
]);
// Con parametri aggiuntivi
$products = $productModel->getEntityCollectionWithRelations(
relations: ['category', 'brand'],
searchKey: 'laptop',
order: ['price' => 'ASC'],
limit: 20,
joinType: JoinType::inner
);
setAVG(), setMax(), setMin(), setSum() con varie combinazioni di parametri (distinct, append, alias, subquery)opAggregationFunction() con tutte le funzioni aggregate disponibilitestFlattenRelationsDotNotation(), testFlattenRelationsNestedArray(), testFlattenRelationsMixedSyntax()testCustomQueryWithJoinAndConditionOnJoinedTable(), testCustomQueryWithMultipleJoins(), testCustomQueryWithManualJoinAndCustomCondition()testCustomQuerySupportsCrossJoin(), testJoinTypeEnumHasAllCases()testQueryAppendColumnForJoinedTables(), testBaseAdapterHasBuildJoinedColumnsMethod()testAllColumnsReturnsQualifiedNameWithTable(), testAllColumnsReturnsAsteriskWithoutTable()testBaseModelHasNestedRelationMethods(), testBaseResultSetHasNestedHydrationMethods()allColumns() per accettare un parametro opzionale $table e restituire table.* quando fornito, invece di *. Questo centralizza la logica di qualificazione delle colonne nell'adapter (dove appartiene concettualmente, essendo formattazione SQL) invece che nella Query. Previene conflitti di nomi colonna sia con JOIN che senza, rendendo le query più robuste. La modifica è backward compatible grazie al parametro opzionale.Router::setMetaUrl() per permettere la sovrascrittura completa del metaUrl, completando l'API esistente che già forniva getMetaUrl(), concatenateMetaUrl() e resetMetaUrl(). Il nuovo metodo offre maggiore flessibilità nella gestione del routing e migliora la testabilità del componente.setMetaUrl(): testSetMetaUrl() verifica l'impostazione corretta del valore, testSetMetaUrlOverwritesPreviousValue() verifica la sovrascrittura completa anche di valori precedentemente concatenaticonfigFramework.php invece di config.php, permettendo ad ogni modulo di avere il proprio config.php senza conflittiBaseAdapterisVariableOfType() rimuovendo enum_exists() dalla condizione OR per evitare TypeError (BaseModel.php:236)buildPropertiesArray() per verificare prima allowsNull() (O(1)) poi isVariableOfType() (più costoso) migliorando le performance (BaseModel.php:217)buildReferencedEntitiesConditions() in buildPropertyConditions() per unificare logica tra entità referenziate e proprietà builtinQuesta patch release corregge un bug critico nella gestione delle entità persistenti all'interno delle SismaCollection.
Corretto un bug nel metodo addOrUpdateIntoEntityCollection() della classe ReferencedEntity che causava errori durante l'inserimento in una SismaCollection di entità già persistenti dopo entità non ancora salvate:
$includedEntity->id === $entity->id falliva quando $includedEntity->id era null (entità non ancora persistita)isset($includedEntity->id) prima del confronto per evitare confronti con valori null // Prima (bug):
if (isset($entity->id) && ($includedEntity->id === $entity->id)) {
$includedEntity = $entity;
$found = true;
}
// Dopo (corretto):
if (isset($entity->id) && isset($includedEntity->id) && ($includedEntity->id === $entity->id)) {
$includedEntity = $entity;
$found = true;
}
Scenario del bug:
id = null)id valorizzato)null === 123 falliva, ma il controllo isset() mancava per $includedEntity->id
Questa patch release migliora la qualità del codice della classe Filter attraverso l'eliminazione di duplicazioni e il riordino dei metodi secondo i principi del Clean Code. Include inoltre la documentazione per la migrazione dalla versione 9.x alla 10.x.
Refactorizzata la classe Filter per eliminare codice duplicato nei metodi di validazione con limiti di lunghezza:
$result e assegnazioni condizionali multiple public function isMinLimitString($value, int $minLimit): bool
{
$result = true;
$result = ($this->isString($value)) ? $result : false;
$result = (strlen($value) >= $minLimit) ? $result : false;
return $result;
}
public function isMinLimitString($value, int $minLimit): bool
{
return $this->isMinLengthForValidator($value, $minLimit, fn($v) => $this->isString($v));
}
private function isMinLengthForValidator(mixed $value, int $minLimit, callable $validator): bool
{
return $validator($value) && strlen($value) >= $minLimit;
}
isMinLengthForValidator(): Valida lunghezza minima con validatore customisMaxLengthForValidator(): Valida lunghezza massima con validatore customisLengthRangeForValidator(): Valida range di lunghezza con validatore customisMinLimitString(), isMaxLimitString(), isLimitString()isMinLimitAlphabeticString(), isMaxLimitAlphabeticString(), isLimitAlphabeticString()isMinLimitAlphanumericString(), isMaxLimitAlphanumericString(), isLimitAlphanumericString()isMinLimitStrictAlphanumericString(), isMaxLimitStrictAlphanumericString(), isLimitStrictAlphanumericString()
Introdotto il file UPGRADING.md con documentazione completa per la migrazione:
CallableController::checkCompatibility() ora metodo staticoCrudInterfaceLanguage::getFriendlyLabel() richiede file di localizzazioneQuesta patch release rifattorizza il DataMapper monolitico introducendo una separazione delle responsabilità in classi dedicate, seguendo i principi SOLID e Clean Code.
Suddiviso il DataMapper monolitico (420 righe) in componenti specializzati per migliorare manutenibilità e testabilità:
getType() privato: Duplicazione logica per determinare tipi di binding nelle querystartTransaction(), commitTransaction(), flag statico $isActiveTransaction)find(), findFirst(), getCount() direttamente nel DataMapper con logica cache integrata@internal): Gestione isolata delle transazioni databasestart(), commit(), rollback()
- Flag di stato transazione centralizzato - Testabile indipendentemente
@internal): Esecuzione query di lettura con integrazione cachefind(), findFirst(), getCount(), setVariable()
- Logica cache isolata e riutilizzabile
- Parametro esplicito bool $ormCacheEnabled passato ai metodi invece di dereferenziare proprietà
getType()
- Riutilizzabile in altri contesti del framework
new in initializers: public function __construct(
?BaseAdapter $adapter = null,
?ProcessedEntitiesCollection $processedEntityCollection = null,
?Config $config = null,
private TransactionManager $transactionManager = new TransactionManager(),
private QueryExecutor $queryExecutor = new QueryExecutor()
)
find(), findFirst(), getCount(), setVariable() → delegati a QueryExecutorsave() → utilizza TransactionManager::start(), commit(), rollback()insert(), update(), delete(), parseValues()) rimangono privati in DataMappersave() → insert()/update() → parseValues() → helper privatiDataType::fromReflection() sostituisce metodo privato getType()@internal: TransactionManager e QueryExecutor sono marcate come interne, non parte dell'API pubblica stabileQuesta patch release corregge un bug importante nella gestione del routing.
Corretto il momento in cui viene impostato l'URL attuale nel Router all'interno del Dispatcher:
Router::setActualCleanUrl() veniva chiamato prima del controllo dell'esistenza dell'action, impostando l'URL anche per azioni inesistentiRouter::setActualCleanUrl() viene chiamato solo dopo aver verificato che l'action esista ed è valida (dentro il blocco if)$this->parsedAction a $this->pathAction per maggiore coerenza con la nomenclaturaImpatto: Previene l'impostazione di URL per azioni non valide, migliorando la precisione del routing e la gestione degli errori 404.
Questa hotfix release corregge i test rotti nella versione 10.0.2.
Ripristinati i mock di BaseAdapter nei test che istanziano DataMapper con costruttore reale:
DispatcherTest.php, ParserTest.php, NotationManagerTest.php, FixturesManagerTest.php, FilterTest.phpBaseFormTest.php, BaseFixtureTest.php
Causa del problema: Questi test creano istanze di DataMapper con costruttore (non completamente mockato), che a sua volta istanzia Query, il cui costruttore chiama BaseAdapter::getDefault(). Senza il mock, getDefault() ritorna null causando errori Call to a member function getAdapterClass() on null.
Soluzione: Ripristinato BaseAdapter::setDefault($baseAdapterMock) in questi test specifici.
Mock rimossi con successo (14 test):
ProcessedEntitiesCollectionTest.php, CacheTest.php, ResultSetMysqlTest.php, SelfReferencedEntityTest.php, ReferencedEntityTest.php, SelfReferencedModelTest.php, DependentModelTest.php, BaseEntityTest.php, BaseModelTest.php, SismaCollectionTest.phpAuthenticationTest.php, BaseVoterTest.php, BasePermissionTest.phpRenderTest.phpMock mantenuti (7 test + 3 specifici ORM):
DispatcherTest.php, ParserTest.php, NotationManagerTest.php, FixturesManagerTest.php, FilterTest.php, BaseFormTest.php, BaseFixtureTest.phpDataMapperTest.php, QueryTest.php, AdapterMysqlTest.php⚠️ NOTA: Questa versione è stata ritirata a causa di test rotti. Utilizzare la versione 10.0.3 invece.
Questa patch release ottimizza significativamente le performance eliminando connessioni al database non necessarie attraverso l'implementazione del lazy loading in BaseAdapter.
Implementato lazy loading della connessione al database in BaseAdapter per evitare connessioni inutili:
select(), execute(), beginTransaction(), etc.)$isConnected (bool) e $connectionOptions (array)ensureConnected() per apertura on-demandselect(), execute(), beginTransaction(), commitTransaction(), rollbackTransaction(), lastInsertId()beginTransactionToDelegateAdapter(), commitTransactionToDelegateAdapter(), rollbackTransactionToDelegateAdapter(), lastInsertIdToDelegateAdapter()beginTransaction() → beginTransactionToDelegateAdapter(), commitTransaction() → commitTransactionToDelegateAdapter(), rollbackTransaction() → rollbackTransactionToDelegateAdapter(), lastInsertId() → lastInsertIdToDelegateAdapter()Impatto sulle performance:
Rimossi 21 mock di BaseAdapter non più necessari grazie al lazy loading:
DispatcherTest.php, ParserTest.php, NotationManagerTest.php, FixturesManagerTest.php, FilterTest.php, RenderTest.phpBaseFormTest.php, BaseFixtureTest.phpProcessedEntitiesCollectionTest.php, CacheTest.php, ResultSetMysqlTest.phpSelfReferencedEntityTest.php, ReferencedEntityTest.php, SelfReferencedModelTest.php, DependentModelTest.phpBaseEntityTest.php, BaseModelTest.php, SismaCollectionTest.phpAuthenticationTest.php, BaseVoterTest.php, BasePermissionTest.phpImpatto: Test più puliti e leggibili, eliminando boilerplate di setup per il mock del database.
Questa patch release corregge un bug nella generazione degli URL con il Router.
Corretto il metodo Router::makeCleanUrl() per gestire correttamente i parametri query string:
?param=value anche quando l'URL aveva già una query stringhttp_build_query() per costruire correttamente la query string e concatenarla con ? o & in base alla presenza di query string esistente nell'URLEsempio:
// Prima (bug):
Router::makeCleanUrl('/search', ['q' => 'test', 'page' => 2])
// Output errato: /search?q=test?page=2
// Dopo (corretto):
Router::makeCleanUrl('/search', ['q' => 'test', 'page' => 2])
// Output corretto: /search?q=test&page=2
Impatto: Risolve problemi di URL malformati quando si passano parametri query string al Router.
Questa major release introduce breaking changes significativi per migliorare la qualità del codice e l'architettura del framework.
Motivazione: Il metodo checkCompatibility() non dovrebbe dipendere dallo stato dell'istanza del controller.
class MyController extends BaseController implements CallableController
{
public function checkCompatibility(array $arguments): bool
{
return count($arguments) === 2;
}
}
class MyController extends BaseController implements CallableController
{
public static function checkCompatibility(array $arguments): bool
{
return count($arguments) === 2;
}
}
Azione richiesta: Aggiungere la keyword static alla firma del metodo checkCompatibility() in tutti i controller che implementano CallableController.
Motivazione: L'interfaccia CrudInterface non forniva valore aggiunto rispetto a BaseController e creava confusione.
class PostController extends BaseController implements CrudInterface
{
// Implementazione
}
class PostController extends BaseController
{
// Implementazione (nessuna modifica ai metodi)
}
Azione richiesta: Rimuovere implements CrudInterface dalla dichiarazione delle classi controller. Nessuna modifica ai metodi è necessaria.
Motivazione: Eliminare valori hardcoded e centralizzare le traduzioni in file di configurazione.
// Funzionava anche senza file di localizzazione
$label = Language::getFriendlyLabel('it');
// Output: "Italiano" (hardcoded)
// Richiede il file config/locales/it.json con:
// {
// "language": {
// "friendly_label": "Italiano"
// }
// }
$label = Language::getFriendlyLabel('it');
// Output: "Italiano" (da file di configurazione)
Azione richiesta:
config/locales/ se non esisteit.json, en.json)
config/locales/it.json:
{
"language": {
"friendly_label": "Italiano"
}
}
config/locales/en.json:
{
"language": {
"friendly_label": "English"
}
}
Per una guida completa alla migrazione dalla versione 9.x alla 10.x, consultare il file UPGRADING.md.
Se hai trovato errori o vuoi suggerire miglioramenti, apri una issue su GitHub.
Report Issue Edit on GitHub