Questa guida copre gli aspetti avanzati dell'ORM di SismaFramework, incluse tecniche per query complesse, ottimizzazione delle performance, gestione delle relazioni e patterns architetturali per applicazioni enterprise.
L'ORM di SismaFramework implementa il pattern Data Mapper, offrendo:
use SismaFramework\Orm\Enumerations\ComparisonOperator;
use SismaFramework\Orm\Enumerations\Placeholder;
use SismaFramework\Orm\Enumerations\DataType;
class AdvancedPostService
{
private PostModel $postModel;
private DataMapper $dataMapper;
public function findPostsByComplexCriteria(
string $titlePattern,
\DateTime $fromDate,
array $authorIds,
bool $isPublished = true
): SismaCollection {
$query = $this->postModel->initQuery();
$bindValues = [];
$bindTypes = [];
$query->setWhere()
// Titolo contiene pattern
->appendCondition('title', ComparisonOperator::like, Placeholder::placeholder)
->appendAnd()
// Data pubblicazione maggiore di
->appendCondition('published_at', ComparisonOperator::greaterThan, Placeholder::placeholder)
->appendAnd()
// Autore in lista
->appendCondition('author_id', ComparisonOperator::in, Placeholder::placeholder)
->appendAnd()
// Status pubblicato
->appendCondition('status', ComparisonOperator::equal, Placeholder::placeholder);
$bindValues = [
"%{$titlePattern}%",
$fromDate->format('Y-m-d H:i:s'),
$authorIds,
$isPublished ? 'published' : 'draft'
];
$bindTypes = [
DataType::typeString,
DataType::typeString,
DataType::typeArrayInteger,
DataType::typeString
];
$query->setOrderBy(['published_at' => 'DESC']);
$query->close();
return $this->dataMapper->find('Post', $query, $bindValues, $bindTypes);
}
public function findPostsWithLogicalGrouping(): SismaCollection
{
$query = $this->postModel->initQuery();
// WHERE (status = 'published' OR status = 'featured')
// AND (view_count > 100 OR comment_count > 10)
$query->setWhere()
->appendOpenBlock()
->appendCondition('status', ComparisonOperator::equal, Placeholder::placeholder)
->appendOr()
->appendCondition('status', ComparisonOperator::equal, Placeholder::placeholder)
->appendCloseBlock()
->appendAnd()
->appendOpenBlock()
->appendCondition('view_count', ComparisonOperator::greaterThan, Placeholder::placeholder)
->appendOr()
->appendCondition('comment_count', ComparisonOperator::greaterThan, Placeholder::placeholder)
->appendCloseBlock();
$bindValues = ['published', 'featured', 100, 10];
$bindTypes = [
DataType::typeString,
DataType::typeString,
DataType::typeInteger,
DataType::typeInteger
];
$query->close();
return $this->dataMapper->find('Post', $query, $bindValues, $bindTypes);
}
}
La ricerca fulltext utilizza l'enumerazione TextSearchMode (introdotta nella 11.3.0) per scegliere la modalità di ricerca MySQL:
TextSearchMode::inNaturaLanguageMode (default) → IN NATURAL LANGUAGE MODETextSearchMode::withQueryExpansion → WITH QUERY EXPANSIONTextSearchMode::inBooleanMode → IN BOOLEAN MODEuse SismaFramework\Orm\Enumerations\TextSearchMode;
class PostSearchService
{
public function searchPosts(string $searchTerms): SismaCollection
{
$query = $this->postModel->initQuery();
// Usa fulltext search su colonne indicizzate
$query->setWhere()
->appendFulltextCondition(['title', 'content'], Placeholder::placeholder, TextSearchMode::inBooleanMode);
$bindValues = [$searchTerms];
$bindTypes = [DataType::typeString];
$query->close();
return $this->dataMapper->find('Post', $query, $bindValues, $bindTypes);
}
public function searchWithRelevanceScore(string $searchTerms): SismaCollection
{
$query = $this->postModel->initQuery();
// Aggiunge score di relevance come colonna
// Firma: setFulltextIndexColumn(array $columns, $value, TextSearchMode $textSearchMode, ?string $columnAlias, bool $append)
$query->setFulltextIndexColumn(['title', 'content'], Placeholder::placeholder, TextSearchMode::inNaturaLanguageMode, 'relevance_score', true)
->setWhere()
->appendFulltextCondition(['title', 'content'], Placeholder::placeholder, TextSearchMode::inNaturaLanguageMode);
$bindValues = [$searchTerms, $searchTerms];
$bindTypes = [DataType::typeString, DataType::typeString];
$query->setOrderBy(['relevance_score' => 'DESC']);
$query->close();
return $this->dataMapper->find('Post', $query, $bindValues, $bindTypes);
}
}
Nota (12.0.0): prima della 12.0.0 il parametro$textSearchModeera l'ultimo argomento disetFulltextIndexColumn(); da questa versione precede$columnAliase$append. Il comandosisma upgradecorregge automaticamente le chiamate esistenti.
Le entità di SismaFramework utilizzano i metodi magici di PHP per implementare funzionalità avanzate in modo trasparente. Questi metodi intercettano operazioni su proprietà per gestire lazy loading, tracking delle modifiche e validazione.
__get($name) - Magic GetterIntercetta l'accesso in lettura alle proprietà per abilitare il lazy loading:
// Esempio di utilizzo
$post = $postModel->getEntityById(1);
$authorName = $post->author->name; // Trigger di __get('author')
// Cosa succede internamente:
// 1. Verifica che la proprietà 'author' esista nella classe finale
// 2. Chiama forceForeignKeyPropertySet('author')
// 3. Se author è un ID non convertito, carica l'entità User dal database
// 4. Ritorna l'istanza di User caricata
Comportamento:
InvalidPropertyException se la proprietà non esisteQuando viene chiamato:
$post->authorisset($post->author) (chiama anche __isset)$post->getAuthor()__set($name, $value) - Magic SetterIntercetta l'assegnazione alle proprietà per gestire il tracking delle modifiche:
// Esempio di utilizzo
$post->author = 5; // Trigger di __set('author', 5)
$post->author = $userEntity; // Trigger di __set('author', $userEntity)
$post->author = null; // Trigger di __set('author', null)
// Cosa succede internamente:
// 1. Verifica che la proprietà esista nella classe finale
// 2. Chiama switchSettingType('author', $value)
// 3. Determina il tipo di valore (int, BaseEntity, null)
// 4. Gestisce la conversione e il tracking delle modifiche
Comportamento:
switchSettingType() per la gestione specifica del tipo$modified quando necessarioProcessedEntitiesCollection se modificataInvalidPropertyException se la proprietà non esisteGestione dei tipi:
$foreignKeyIndexes per lazy loading__isset($name) - Magic IssetIntercetta i controlli di esistenza delle proprietà:
// Esempio di utilizzo
if (isset($post->author)) {
echo $post->author->name;
}
// Cosa succede internamente:
// 1. Verifica che la proprietà esista nella classe finale
// 2. Attiva il lazy loading se necessario
// 3. Ritorna true/false in base allo stato della proprietà
Comportamento:
Casi d'uso:
Il sistema di tracking monitora tutte le modifiche alle proprietà per determinare se l'entità deve essere salvata:
$post = $postModel->getEntityById(1);
echo $post->modified; // false - appena caricata
$post->title = "Nuovo Titolo";
echo $post->modified; // true - proprietà modificata
$post->author = 5;
echo $post->modified; // true - foreign key modificata
Quando una proprietà è considerata modificata:
equals())
Il tracking si integra con ProcessedEntitiesCollection per evitare duplicazioni nel salvataggio:
// Prima modifica: entità rimossa dalla collection
$post->title = "Titolo A";
// $processedEntitiesCollection->remove($post) chiamato automaticamente
// Il DataMapper può ora salvare l'entità anche se già processata
$dataMapper->save($post);
class EntityMagicMethodsDemo
{
public function demonstrateMagicMethods(): void
{
$postModel = new PostModel($this->dataMapper);
// Carica post (lazy loading pronto)
$post = $postModel->getEntityById(1);
// __isset: verifica esistenza autore
if (isset($post->author)) {
// __get: attiva lazy loading e carica User
echo "Autore: " . $post->author->name;
}
// __set: assegna nuovo autore (tracking attivato)
$post->author = 5;
echo $post->modified; // true
// __set: assegna entità completa
$newAuthor = new User();
$newAuthor->id = 5;
$newAuthor->name = "Mario Rossi";
$post->author = $newAuthor; // Nessuna query, entità già in memoria
// __set: rimuove relazione
$post->author = null;
// Salva le modifiche
$this->dataMapper->save($post);
}
public function demonstratePerformanceImpact(): void
{
$posts = $this->postModel->getEntityCollection();
// ❌ INEFFICIENTE: __get chiamato in loop, N+1 queries
foreach ($posts as $post) {
if (isset($post->author)) { // __isset + __get
echo $post->author->name; // Lazy loading per ogni post
}
}
// ✅ EFFICIENTE: Batch loading
$this->batchLoadAuthors($posts);
foreach ($posts as $post) {
if (isset($post->author)) {
echo $post->author->name; // Nessuna query, già caricato
}
}
}
}
I DependentModel e SelfDependentModel di SismaFramework implementano il metodo magico __call() per generare dinamicamente metodi di query basati sulle relazioni tra entità. Questi metodi non esistono fisicamente nel codice ma vengono creati al volo interpretando il nome del metodo chiamato.
Il metodo __call() intercetta chiamate a metodi inesistenti e li interpreta seguendo questa convenzione di naming:
{azione}By{Proprietà}[And{AltraProprietà}]*
Azioni supportate:
get → Ritorna SismaCollectioncount → Ritorna intdelete → Ritorna boolEsempi di metodi generati automaticamente:
// Esempio di entità Post con foreign keys
class Post extends DependentEntity
{
public int $id;
public string $title;
public User $author; // Foreign key
public Category $category; // Foreign key
public Status $status; // Enum
}
class PostModel extends DependentModel
{
// NESSUN METODO DEFINITO ESPLICITAMENTE!
// Tutti i metodi sotto vengono generati al volo da __call()
}
// Utilizzo:
$postModel = new PostModel($dataMapper);
$user = $userModel->getEntityById(5);
$category = $categoryModel->getEntityById(3);
// 1. Query per singola foreign key
$posts = $postModel->getByAuthor($user);
// SELECT * FROM post WHERE author_id = 5
// 2. Count per singola foreign key
$count = $postModel->countByAuthor($user);
// SELECT COUNT(*) FROM post WHERE author_id = 5
// 3. Delete per singola foreign key
$deleted = $postModel->deleteByAuthor($user);
// DELETE FROM post WHERE author_id = 5
// 4. Query per multiple foreign keys (AND)
$posts = $postModel->getByAuthorAndCategory($user, $category);
// SELECT * FROM post WHERE author_id = 5 AND category_id = 3
// 5. Count per multiple foreign keys
$count = $postModel->countByAuthorAndCategory($user, $category);
// SELECT COUNT(*) FROM post WHERE author_id = 5 AND category_id = 3
// 6. Delete per multiple foreign keys
$deleted = $postModel->deleteByAuthorAndCategory($user, $category);
// DELETE FROM post WHERE author_id = 5 AND category_id = 3
// 7. Query con foreign key null
$orphanedPosts = $postModel->getByCategory(null);
// SELECT * FROM post WHERE category_id IS NULL
Il metodo __call() esegue questi step:
getByAuthorAndCategory → ['get', 'AuthorAndCategory']get → determina l'operazione (get/count/delete)AuthorAndCategory → ['Author', 'Category']Author → author, Category → category
// Pattern generale:
public function {azione}By{Proprietà}[And{AltraProprietà}]*(
BaseEntity|null ${proprietà},
[BaseEntity|null ${altraProprietà},]*
?string $searchKey = null,
?array $order = null, // Solo per get*
?int $offset = null, // Solo per get*
?int $limit = null // Solo per get*
): SismaCollection|int|bool;
// Esempi concreti:
public function getByAuthor(
User|null $author,
?string $searchKey = null,
?array $order = null,
?int $offset = null,
?int $limit = null
): SismaCollection;
public function countByAuthorAndCategory(
User|null $author,
Category|null $category,
?string $searchKey = null
): int;
public function deleteByStatus(
Status|null $status,
?string $searchKey = null
): bool;
Il sistema valida automaticamente che gli argomenti passati corrispondano al tipo delle foreign keys:
// ✅ VALIDO
$user = new User();
$posts = $postModel->getByAuthor($user);
// ✅ VALIDO (null per IS NULL)
$posts = $postModel->getByAuthor(null);
// ❌ INVALIDO - TypeError
$category = new Category();
$posts = $postModel->getByAuthor($category); // Expects User, got Category
// Lancia InvalidArgumentException
Tutti i metodi magici supportano il parametro opzionale $searchKey per filtrare ulteriormente:
class PostModel extends DependentModel
{
protected function appendSearchCondition(
Query &$query,
string $searchKey,
array &$bindValues,
array &$bindTypes
): void {
// Ricerca nel titolo
$query->appendCondition('title', ComparisonOperator::like, Placeholder::placeholder);
$bindValues[] = "%{$searchKey}%";
$bindTypes[] = DataType::typeString;
}
}
// Utilizzo:
$posts = $postModel->getByAuthor($user, 'framework');
// SELECT * FROM post WHERE author_id = 5 AND title LIKE '%framework%'
$count = $postModel->countByCategory($category, 'tutorial');
// SELECT COUNT(*) FROM post WHERE category_id = 3 AND title LIKE '%tutorial%'
class RelationalQueryService
{
public function getUserContent(User $user): array
{
$postModel = new PostModel($this->dataMapper);
$commentModel = new CommentModel($this->dataMapper);
// Metodi magici generati dinamicamente!
$userPosts = $postModel->getByAuthor($user);
$userComments = $commentModel->getByAuthor($user);
$stats = [
'posts_count' => $postModel->countByAuthor($user),
'comments_count' => $commentModel->countByAuthor($user),
'posts' => $userPosts,
'comments' => $userComments
];
return $stats;
}
public function getPostsByCategory(Category $category): SismaCollection
{
$postModel = new PostModel($this->dataMapper);
// Metodo magico: getByCategory()
return $postModel->getByCategory($category);
}
public function getCommentsByPost(Post $post): SismaCollection
{
$commentModel = new CommentModel($this->dataMapper);
// Metodo magico: getByPost()
return $commentModel->getByPost($post);
}
public function getFeaturedPostsByAuthor(User $author): SismaCollection
{
$postModel = new PostModel($this->dataMapper);
$featuredStatus = Status::Featured;
// Metodo magico con AND: getByAuthorAndStatus()
return $postModel->getByAuthorAndStatus($author, $featuredStatus);
}
public function bulkDeleteUserContent(User $user): bool
{
$postModel = new PostModel($this->dataMapper);
$commentModel = new CommentModel($this->dataMapper);
$this->dataMapper->beginTransaction();
try {
// Metodi magici: deleteByAuthor()
$postModel->deleteByAuthor($user);
$commentModel->deleteByAuthor($user);
$this->dataMapper->commit();
return true;
} catch (\Exception $e) {
$this->dataMapper->rollback();
throw $e;
}
}
public function getOrphanedComments(): SismaCollection
{
$commentModel = new CommentModel($this->dataMapper);
// Metodo magico con null: getByPost(null)
// Trova commenti dove post_id IS NULL
return $commentModel->getByPost(null);
}
}
✅ Zero boilerplate: Nessun metodo da scrivere manualmente ✅ Type-safe: Validazione automatica dei tipi ✅ Consistenza: Naming convention uniforme ✅ Flessibilità: Supporta combinazioni multiple con AND ✅ Null-safe: Gestione automatica di IS NULL
⚠️ Nessun IDE autocomplete: I metodi non esistono fisicamente ⚠️ Solo AND logic: Non supporta OR tra foreign keys ⚠️ Convention rigida: Naming deve seguire lo schema esatto ⚠️ Debugging: Stack trace meno chiaro per errori nei metodi magici
/**
* @method SismaCollection getByAuthor(User $author, ?string $searchKey = null, ?array $order = null, ?int $offset = null, ?int $limit = null)
* @method int countByAuthor(User $author, ?string $searchKey = null)
* @method bool deleteByAuthor(User $author, ?string $searchKey = null)
* @method SismaCollection getByCategory(Category $category, ?string $searchKey = null, ?array $order = null, ?int $offset = null, ?int $limit = null)
*/
class PostModel extends DependentModel
{
// ...
}
Le entità che estendono ReferencedEntity hanno accesso a metodi magici aggiuntivi per gestire le collezioni inverse (one-to-many). Questi metodi sono generati dinamicamente tramite __call() e permettono di manipolare le entità figlie senza scrivere codice boilerplate.
Quando un'entità è referenziata da altre entità (es. User referenziato da Post.author), la ReferencedEntity espone automaticamente una collezione inversa accessibile tramite una naming convention:
// Naming convention per le collezioni:
// {nomeEntitàFiglia}Collection{NomeProprietàForeignKey}
// Esempio: User ha molti Post tramite Post.author
$user->postCollectionAuthor; // SismaCollection di Post
// Se c'è una sola foreign key, il suffisso può essere omesso:
$user->postCollection; // Equivalente se Post ha solo 'author' come FK verso User
Il metodo __call() in ReferencedEntity intercetta tre pattern di chiamata:
1. set{CollectionName}(SismaCollection $collection) - Sostituisce la collezione
$user = $userModel->getEntityById(1);
// Crea una nuova collezione di post
$newPosts = new SismaCollection(Post::class);
$newPosts->append($post1);
$newPosts->append($post2);
// Sostituisce tutti i post dell'utente
$user->setPostCollectionAuthor($newPosts);
// Internamente:
// - La collezione viene assegnata
// - Per ogni post nella collezione, $post->author = $user viene impostato automaticamente
2. add{EntityName}(BaseEntity $entity) - Aggiunge un'entità alla collezione
$user = $userModel->getEntityById(1);
$newPost = new Post();
$newPost->title = "Nuovo articolo";
// Aggiunge il post alla collezione dell'utente
$user->addPost($newPost);
// Internamente:
// - Se esiste già un post con lo stesso ID, viene sostituito
// - Altrimenti viene aggiunto alla collezione
// - $newPost->author = $user viene impostato automaticamente
3. count{CollectionName}() - Conta le entità nella collezione
$user = $userModel->getEntityById(1);
// Conta i post dell'utente (esegue una query COUNT)
$postCount = $user->countPostCollectionAuthor();
// Internamente:
// - Viene creato il Model corrispondente (PostModel)
// - Viene chiamato il metodo magico countBy{ForeignKey}() (es. countByAuthor())
// - Ritorna il numero intero di risultati
Oltre ai metodi magici, le collezioni sono accessibili anche tramite __get() e __set():
// Lettura (trigger lazy loading se necessario)
$posts = $user->postCollectionAuthor;
// Scrittura diretta
$user->postCollectionAuthor = $newPostsCollection;
// Verifica esistenza
if (isset($user->postCollectionAuthor)) {
foreach ($user->postCollectionAuthor as $post) {
echo $post->title;
}
}
class UserService
{
public function demonstrateCollectionMethods(int $userId): void
{
$userModel = new UserModel($this->dataMapper);
$user = $userModel->getEntityById($userId);
// 1. Conta i post (query COUNT, efficiente)
$totalPosts = $user->countPostCollectionAuthor();
echo "L'utente ha {$totalPosts} post";
// 2. Accedi alla collezione (lazy loading)
$posts = $user->postCollectionAuthor;
foreach ($posts as $post) {
echo $post->title;
}
// 3. Aggiungi un nuovo post
$newPost = new Post();
$newPost->title = "Post aggiunto tramite metodo magico";
$newPost->content = "Contenuto...";
$user->addPost($newPost);
// $newPost->author è ora automaticamente $user
// 4. Sostituisci l'intera collezione
$selectedPosts = new SismaCollection(Post::class);
$selectedPosts->append($posts[0]);
$user->setPostCollectionAuthor($selectedPosts);
// 5. Salva le modifiche
$this->dataMapper->save($user);
}
public function getAuthorWithPostCount(int $userId): array
{
$userModel = new UserModel($this->dataMapper);
$user = $userModel->getEntityById($userId);
return [
'user' => $user,
'post_count' => $user->countPostCollectionAuthor(),
'comment_count' => $user->countCommentCollectionAuthor(),
];
}
}
Il sistema valida automaticamente i tipi:
// ✅ VALIDO
$user->setPostCollectionAuthor(new SismaCollection(Post::class));
// ❌ INVALIDO - Tipo errato
$user->setPostCollectionAuthor(new SismaCollection(Comment::class));
// Lancia InvalidArgumentException
// ✅ VALIDO
$user->addPost(new Post());
// ❌ INVALIDO - Non è un'entità
$user->addPost("stringa");
// Lancia InvalidArgumentException
count e ->count()// countPostCollectionAuthor() - Query COUNT sul database
// Efficiente, non carica le entità in memoria
$count = $user->countPostCollectionAuthor();
// postCollectionAuthor->count() - Conta elementi già caricati
// Richiede prima il lazy loading di tutte le entità
$count = $user->postCollectionAuthor->count();
Per grandi collezioni, preferire sempre count{CollectionName}() per evitare di caricare tutte le entità in memoria.
Il lazy loading in SismaFramework è implementato attraverso i metodi magici di PHP (__get, __set, __isset) nella classe BaseEntity. Questo meccanismo consente di caricare le relazioni solo quando vengono effettivamente accessate, migliorando significativamente le performance.
Quando un'entità viene caricata dal database, le foreign key vengono inizialmente memorizzate come semplici ID nell'array $foreignKeyIndexes, senza caricare le entità correlate. Solo quando si accede alla proprietà, il metodo magico __get() intercetta la richiesta e:
$foreignKeyIndexes
Una foreign key può trovarsi in tre stati:
// Stato 1: ID non convertito (lazy loading pronto)
$post->author = 5; // Assegna l'ID intero
// Internamente: $foreignKeyIndexes['author'] = 5
// Stato 2: Entità caricata
$author = $post->author; // Trigger del lazy loading
// Internamente: $author è ora un'istanza di User, foreignKeyIndexes['author'] rimosso
// Stato 3: Cache hit
$post->author = 5; // Stesso ID già in cache
// L'entità viene recuperata dalla cache invece che dal database
switchSettingType()
Il metodo switchSettingType() nella classe BaseEntity gestisce tre scenari quando si imposta una foreign key:
1. Assegnazione di un ID intero:
$post->author = 5;
// Internamente:
// - Se l'entità User con ID=5 è già in cache, la recupera immediatamente
// - Altrimenti, memorizza l'ID in $foreignKeyIndexes['author'] = 5
// - La proprietà $author viene unset (lazy loading attivato)
// - Al primo accesso, __get() caricherà l'entità dal database
2. Assegnazione di un'entità:
$author = new User();
$author->id = 5;
$post->author = $author;
// Internamente:
// - L'entità viene assegnata direttamente alla proprietà
// - L'ID viene rimosso da $foreignKeyIndexes se presente
// - Il sistema di tracking 'modified' viene aggiornato
3. Assegnazione null:
$post->author = null;
// Internamente:
// - La proprietà viene impostata a null
// - L'ID viene rimosso da $foreignKeyIndexes
// - Il sistema di tracking 'modified' viene aggiornato
✅ Performance: Carica solo i dati effettivamente necessari ✅ Semplicità: API trasparente, nessun metodo speciale da chiamare ✅ Memoria: Riduce il footprint in memoria per relazioni non utilizzate ✅ Caching: Integrazione automatica con il sistema di cache delle entità
⚠️ Problema N+1:
// ❌ INEFFICIENTE: Query per ogni post
$posts = $postModel->getEntityCollection();
foreach ($posts as $post) {
echo $post->author->name; // Query separata per ogni autore!
}
✅ Soluzione - Batch Loading:
class LazyLoadingExample
{
public function demonstrateLazyLoading(): void
{
$postModel = new PostModel($this->dataMapper);
// Carica solo il post
$post = $postModel->getEntityById(1);
// Lazy loading automatico dell'autore
$authorName = $post->author->name; // Query automatica
// Lazy loading della collezione commenti
$comments = $post->commentCollection; // Query automatica
}
public function optimizedBatchLoading(): void
{
$postModel = new PostModel($this->dataMapper);
$userModel = new UserModel($this->dataMapper);
// Carica tutti i post
$posts = $postModel->getEntityCollection();
// Estrae IDs degli autori dai foreignKeyIndexes
$authorIds = [];
foreach ($posts as $post) {
$foreignKeyIndexes = $post->getForeignKeyIndexes();
if (isset($foreignKeyIndexes['author'])) {
$authorIds[] = $foreignKeyIndexes['author'];
}
}
$authorIds = array_unique($authorIds);
// Carica tutti gli autori in una query (batch loading)
$authors = $userModel->getEntityCollectionByIds($authorIds);
// Crea mappa per lookup veloce O(1)
$authorMap = [];
foreach ($authors as $author) {
$authorMap[$author->id] = $author;
}
// Assegna gli autori pre-caricati ai post
foreach ($posts as $post) {
$foreignKeyIndexes = $post->getForeignKeyIndexes();
if (isset($foreignKeyIndexes['author']) && isset($authorMap[$foreignKeyIndexes['author']])) {
$post->author = $authorMap[$foreignKeyIndexes['author']];
}
}
// Ora l'accesso all'autore non genera query
foreach ($posts as $post) {
echo $post->author->name; // Nessuna query aggiuntiva
}
}
}
Il lazy loading si integra automaticamente con il sistema di cache delle entità:
use SismaFramework\Orm\HelperClasses\Cache;
// Prima richiesta: carica dal database e mette in cache
$post->author; // SELECT * FROM user WHERE id = 5
// Successive richieste: recupera dalla cache
$post->author; // Nessuna query - cache hit!
// Verifica presenza in cache
if (Cache::checkEntityPresenceInCache(User::class, 5)) {
$user = Cache::getEntityById(User::class, 5);
}
class CachedPostService
{
private PostModel $postModel;
private CacheInterface $cache;
public function getPopularPosts(int $limit = 10): SismaCollection
{
$cacheKey = "popular_posts_{$limit}";
$cachedPosts = $this->cache->get($cacheKey);
if ($cachedPosts !== null) {
return $cachedPosts;
}
$query = $this->postModel->initQuery();
$query->setOrderBy(['view_count' => 'DESC'])
->setLimit($limit);
$query->close();
$posts = $this->dataMapper->find('Post', $query);
$this->cache->set($cacheKey, $posts, 3600); // Cache 1 ora
return $posts;
}
public function invalidatePostCache(Post $post): void
{
// Invalida cache correlate
$this->cache->delete("post_{$post->getId()}");
$this->cache->delete("popular_posts_*");
$this->cache->delete("author_posts_{$post->getAuthorId()}");
}
}
class BatchOperationService
{
public function bulkUpdateViewCounts(array $postIds): bool
{
// ❌ INEFFICIENTE: Query singole
foreach ($postIds as $id) {
$post = $this->postModel->getEntityById($id);
$post->incrementViewCount();
$this->dataMapper->save($post);
}
// ✅ EFFICIENTE: Query batch
$query = $this->postModel->initQuery();
$query->setWhere()
->appendCondition('id', ComparisonOperator::in, Placeholder::placeholder);
// Per operazioni batch personalizzate, usa deleteBatch o SQL raw tramite l'adapter
$query->close();
$bindValues = [$postIds];
$bindTypes = [DataType::typeArrayInteger];
// Esempio: usa l'adapter direttamente per SQL personalizzato
$sql = "UPDATE post SET view_count = view_count + 1 WHERE id IN (?)";
return $this->dataMapper->getAdapter()->execute($sql, $bindValues, $bindTypes);
}
public function createMultiplePosts(array $postsData): SismaCollection
{
$posts = new SismaCollection('Post');
$this->dataMapper->beginTransaction();
try {
foreach ($postsData as $data) {
$post = new Post();
$post->setTitle($data['title']);
$post->setContent($data['content']);
$post->setAuthorId($data['author_id']);
$this->dataMapper->save($post);
$posts->append($post);
}
$this->dataMapper->commit();
return $posts;
} catch (\Exception $e) {
$this->dataMapper->rollback();
throw $e;
}
}
}
class StatisticsService
{
public function getContentStatistics(): array
{
$postModel = new PostModel($this->dataMapper);
$userModel = new UserModel($this->dataMapper);
$commentModel = new CommentModel($this->dataMapper);
return [
'total_posts' => $postModel->countEntityCollection(),
'published_posts' => $this->countPublishedPosts(),
'total_users' => $userModel->countEntityCollection(),
'total_comments' => $commentModel->countEntityCollection(),
'posts_per_author' => $this->getPostsPerAuthor()
];
}
private function countPublishedPosts(): int
{
$query = $this->postModel->initQuery();
$query->setWhere()
->appendCondition('status', ComparisonOperator::equal, Placeholder::placeholder);
$bindValues = ['published'];
$bindTypes = [DataType::typeString];
$query->close();
return $this->dataMapper->getCount($query, $bindValues, $bindTypes);
}
private function getPostsPerAuthor(): SismaCollection
{
// Per aggregazioni, crea un'entità specifica per i risultati
$query = $this->postModel->initQuery();
$query->setColumns(['author_id', 'COUNT(*) as post_count'])
->setGroupBy(['author_id'])
->setOrderBy(['post_count' => 'DESC']);
$query->close();
// Restituisce sempre una SismaCollection di entità tipizzate
return $this->dataMapper->find(AuthorPostStats::class, $query);
}
}
interface PostRepositoryInterface
{
public function findById(int $id): ?Post;
public function findByAuthor(User $author): SismaCollection;
public function findPublished(): SismaCollection;
public function findByDateRange(\DateTime $from, \DateTime $to): SismaCollection;
public function save(Post $post): bool;
public function delete(Post $post): bool;
}
class PostRepository implements PostRepositoryInterface
{
private PostModel $model;
private DataMapper $dataMapper;
public function __construct(DataMapper $dataMapper)
{
$this->dataMapper = $dataMapper;
$this->model = new PostModel($dataMapper);
}
public function findById(int $id): ?Post
{
return $this->model->getEntityById($id);
}
public function findByAuthor(User $author): SismaCollection
{
return $this->model->getByAuthor($author);
}
public function findPublished(): SismaCollection
{
$query = $this->model->initQuery();
$query->setWhere()
->appendCondition('status', ComparisonOperator::equal, Placeholder::placeholder)
->appendAnd()
->appendCondition('published_at', ComparisonOperator::lessThanOrEqual, Placeholder::placeholder);
$bindValues = ['published', new \DateTime()];
$bindTypes = [DataType::typeString, DataType::typeDateTime];
$query->setOrderBy(['published_at' => 'DESC']);
$query->close();
return $this->dataMapper->find('Post', $query, $bindValues, $bindTypes);
}
public function findByDateRange(\DateTime $from, \DateTime $to): SismaCollection
{
$query = $this->model->initQuery();
$query->setWhere()
->appendCondition('published_at', ComparisonOperator::greaterThanOrEqual, Placeholder::placeholder)
->appendAnd()
->appendCondition('published_at', ComparisonOperator::lessThanOrEqual, Placeholder::placeholder);
$bindValues = [$from, $to];
$bindTypes = [DataType::typeDateTime, DataType::typeDateTime];
$query->close();
return $this->dataMapper->find('Post', $query, $bindValues, $bindTypes);
}
public function save(Post $post): bool
{
return $this->dataMapper->save($post);
}
public function delete(Post $post): bool
{
return $this->dataMapper->delete($post);
}
}
class BlogService
{
private PostRepository $postRepository;
private UserRepository $userRepository;
private NotificationService $notificationService;
public function publishPost(int $postId, int $authorId): bool
{
$post = $this->postRepository->findById($postId);
$author = $this->userRepository->findById($authorId);
if (!$post || !$author) {
throw new \InvalidArgumentException('Post or Author not found');
}
if ($post->getAuthorId() !== $authorId) {
throw new \UnauthorizedAccessException('Author mismatch');
}
// Domain logic
$post->publish();
$success = $this->postRepository->save($post);
if ($success) {
// Side effects
$this->notificationService->notifySubscribers($post);
}
return $success;
}
public function getAuthorStatistics(User $author): array
{
$posts = $this->postRepository->findByAuthor($author);
$stats = [
'total_posts' => $posts->count(),
'published_posts' => 0,
'draft_posts' => 0,
'total_views' => 0
];
foreach ($posts as $post) {
if ($post->isPublished()) {
$stats['published_posts']++;
} else {
$stats['draft_posts']++;
}
$stats['total_views'] += $post->getViewCount();
}
return $stats;
}
}
class TransactionalService
{
private DataMapper $dataMapper;
public function transferPostOwnership(Post $post, User $newOwner): bool
{
$this->dataMapper->beginTransaction();
try {
// Aggiorna il post
$post->setAuthorId($newOwner->getId());
$this->dataMapper->save($post);
// Log del trasferimento
$transferLog = new OwnershipTransfer();
$transferLog->setPostId($post->getId());
$transferLog->setOldOwnerId($post->getAuthorId());
$transferLog->setNewOwnerId($newOwner->getId());
$transferLog->setTransferDate(new \DateTime());
$this->dataMapper->save($transferLog);
// Aggiorna statistiche
$this->updateAuthorStatistics($post->getAuthorId(), -1);
$this->updateAuthorStatistics($newOwner->getId(), +1);
$this->dataMapper->commit();
return true;
} catch (\Exception $e) {
$this->dataMapper->rollback();
error_log("Transfer failed: " . $e->getMessage());
return false;
}
}
private function updateAuthorStatistics(int $userId, int $delta): void
{
$userStats = $this->getUserStats($userId);
$userStats->setPostCount($userStats->getPostCount() + $delta);
$this->dataMapper->save($userStats);
}
}
class PostValidationService
{
public function validatePost(Post $post): array
{
$errors = [];
// Validazione base
if (empty($post->getTitle())) {
$errors[] = 'Title is required';
}
if (strlen($post->getTitle()) > 255) {
$errors[] = 'Title too long';
}
// Validazione business rules
if ($post->isPublished() && empty($post->getContent())) {
$errors[] = 'Content required for published posts';
}
// Validazione relazioni
if (!$this->isValidAuthor($post->getAuthorId())) {
$errors[] = 'Invalid author';
}
return $errors;
}
private function isValidAuthor(int $authorId): bool
{
$userModel = new UserModel($this->dataMapper);
$author = $userModel->getEntityById($authorId);
return $author !== null && $author->isActive();
}
}
L'ORM di SismaFramework non supporta JOIN espliciti nel query builder. Questo è una scelta architetturale del pattern Data Mapper che ha sia vantaggi che svantaggi:
✅ Vantaggi:
⚠️ Limitazioni:
💡 Alternative e Workaround:
class ReportingService
{
// 1. SQL Raw per query complesse - usa l'adapter direttamente
public function getPostsWithAuthorNames(): array
{
$sql = "
SELECT p.*, u.name as author_name
FROM post p
INNER JOIN user u ON p.author_id = u.id
WHERE p.status = 'published'
ORDER BY p.published_at DESC
";
$adapter = BaseAdapter::getDefault();
$result = $adapter->select($sql);
return $result ? $result->fetchAll() : [];
}
// 2. Caricamento batch per evitare N+1
public function getPostsWithAuthorsOptimized(): array
{
$postModel = new PostModel($this->dataMapper);
$userModel = new UserModel($this->dataMapper);
// Carica post
$posts = $postModel->getEntityCollection();
// Estrae author IDs
$authorIds = array_unique(
array_map(fn($post) => $post->getAuthorId(), $posts->toArray())
);
// Carica autori in batch
$authors = $userModel->getEntityCollectionByIds($authorIds);
$authorMap = $authors->indexBy('id');
// Combina risultati
$result = [];
foreach ($posts as $post) {
$result[] = [
'post' => $post,
'author' => $authorMap[$post->getAuthorId()]
];
}
return $result;
}
// 3. View materializzate per reporting
public function getPostStatistics(): array
{
// Usa una view pre-calcolata tramite l'adapter
$sql = "SELECT * FROM post_statistics_view";
$adapter = BaseAdapter::getDefault();
$result = $adapter->select($sql);
return $result ? $result->fetchAll() : [];
}
}
È appropriato usare SQL raw quando:
Indice | Precedente: Configuration Reference | Successivo: Performance Guide
Se hai trovato errori o vuoi suggerire miglioramenti, apri una issue su GitHub.
Report Issue Edit on GitHub