All notable changes to this project will be documented in this file.
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