L'Object-Relational Mapper (ORM) di SismaFramework è un potente componente che ti permette di interagire con il database utilizzando oggetti PHP, invece di scrivere query SQL manualmente.
L'ORM si basa sul pattern Data Mapper, che garantisce una netta separazione tra la logica di business (gli oggetti del tuo dominio) e la logica di persistenza dei dati (come vengono salvati e recuperati dal database).
Per tecniche avanzate e patterns complessi, consulta la guida ORM avanzata. Per la documentazione API completa, vedi API Reference.
L'interazione con l'ORM si basa su due tipi di classi principali:
Le entità sono il cuore del tuo modello di dominio. Devono estendere una delle classi base fornite dall'ORM (BaseEntity, ReferencedEntity, SelfReferencedEntity).
Le proprietà di un'entità devono essere dichiarate protected e la tipizzazione è obbligatoria. I tipi supportati sono:
int, string, float, bool).SismaDateTime, SismaDate, SismaTime per date e orari.SismaJson per le colonne JSON (vedi Colonne JSON).BackedEnum per i vocabolari chiusi.Entity per le relazioni (chiavi esterne).namespace MyModule\App\Entities;
use SismaFramework\Orm\BaseClasses\BaseEntity;
use SismaFramework\Orm\CustomTypes\SismaDateTime;
class Post extends BaseEntity
{
protected int $id;
protected string $title;
protected ?string $content = null;
protected SismaDateTime $publicationDate;
protected User $author; // Relazione con l'entità User
protected function setEncryptedProperties(): void { }
protected function setPropertyDefaultValue(): void { }
}
// Questa entità mappa automaticamente alla tabella 'post' (singolare)
// SismaFramework utilizza convenzioni con nomi di tabelle al singolare
Una proprietà tipizzata SismaJson mappa una colonna JSON (MySQL, MariaDB) o testuale contenente un oggetto o un array JSON. Il valore letto dal database viene decodificato in un array associativo; in scrittura viene serializzato con JSON_UNESCAPED_UNICODE, JSON_UNESCAPED_SLASHES e JSON_PRESERVE_ZERO_FRACTION.
use SismaFramework\Orm\CustomTypes\SismaJson;
class Product extends BaseEntity
{
protected int $id;
protected SismaJson $attributes;
protected ?SismaJson $metadata = null;
// ...
}
$product->attributes = new SismaJson(['color' => 'red', 'sizes' => ['S', 'M']]);
$product->attributes = SismaJson::fromJson('{"color":"red"}');
echo $product->attributes['color'];
echo $product->attributes->get('weight', 0);
foreach ($product->attributes as $key => $value) { /* ... */ }
SismaJson è immutabile: per modificarne il contenuto si usano with() e without(), che restituiscono una nuova istanza da riassegnare alla proprietà. La riassegnazione passa dal tracciamento delle modifiche dell'entità, che altrimenti non rileverebbe un cambiamento fatto sull'oggetto in place. L'assegnazione tramite indice ($json['key'] = ...) solleva una LogicException.
$product->attributes = $product->attributes->with('color', 'blue');
$product->attributes = $product->attributes->without('sizes');
Il confronto usato dal tracciamento delle modifiche (equals()) ignora l'ordine delle chiavi degli oggetti, che MySQL riordina in fase di salvataggio, ma non quello degli elementi delle liste, ed è stretto sui tipi (1 e "1" sono diversi).
Limitazioni:
"testo", 5) solleva una JsonException in fromJson() e una InvalidArgumentException durante l'idratazione dell'entità e il parsing di form e argomenti delle action. Fa eccezione il letterale JSON null, che su una proprietà nullable (?SismaJson) diventa null, come il NULL SQL; un successivo salvataggio della colonna scrive quindi NULL SQL al posto del letterale JSON;{} viene riscritto come [], e un oggetto con chiavi numeriche consecutive a partire da 0 viene riscritto come lista;SismaJson cifrata con addEncryptedProperty() viene salvata come stringa cifrata, che non è JSON valido: la colonna deve essere di tipo testuale e non JSON;
Le proprietà dichiarate in setEncryptedProperties() con addEncryptedProperty() vengono cifrate con Encryptor::encryptString() al salvataggio e decifrate alla lettura, in modo trasparente per il codice che usa l'entità.
class Certificate extends BaseEntity
{
protected int $id;
protected string $certificate;
protected ?string $privateKey = null;
protected ?string $privateKeyInitializationVector = null;
protected function setEncryptedProperties(): void
{
$this->addEncryptedProperty('privateKey');
}
protected function setPropertyDefaultValue(): void { }
}
InitializationVector (configurabile con ENCRYPTED_PROPERTY_INITIALIZATION_VECTOR_SUFFIX), e la tabella la colonna corrispondente, binaria e nullable (private_key_initialization_vector). L'ORM genera un nuovo IV a ogni salvataggio della proprietà e lo scrive insieme al valore cifrato: proprietà diverse e versioni successive della stessa proprietà non condividono mai l'IV. Senza vettore d'inizializzazione la cifratura non avviene: le proprietà dichiarate cifrate vengono salvate in chiaro, senza errori né avvisi.initializationVector (colonna initialization_vector), condivisa da tutte le proprietà cifrate dell'entità e rigenerata a ogni salvataggio. Se l'entità è stata caricata solo in parte e una delle proprietà cifrate che usano l'IV condiviso non è inizializzata, l'IV caricato viene mantenuto, perché rigenerarlo renderebbe illeggibile la proprietà non riscritta; se invece l'IV condiviso non è stato caricato, ne viene generato uno nuovo e le proprietà cifrate non caricate non sono più leggibili. Con un IV condiviso, due proprietà della stessa riga con lo stesso valore, o con gli stessi primi 16 byte, producono lo stesso testo cifrato, o lo stesso primo blocco.<proprietà>_initialization_vector (nullable) e all'entità le proprietà corrispondenti, lasciando per il momento initializationVector. Le righe esistenti, che hanno il vettore proprio a NULL, vengono decifrate con l'IV condiviso; al primo salvataggio ogni proprietà viene ricifrata con il proprio IV. Quando nessuna riga ha più il vettore proprio a NULL per le proprietà valorizzate, initializationVector e la sua colonna possono essere rimossi.Query::appendConditionOnEncryptedColumn() riceve il nome della colonna del vettore d'inizializzazione: con il vettore per proprietà va indicata la colonna propria (private_key_initialization_vector), non initialization_vector.ENCRYPTION_PASSPHRASE; con la passphrase vuota le proprietà vengono salvate in chiaro.null viene salvata come NULL e letta come null, senza cifratura né decifratura. Prima della 12.8.1 il salvataggio e la lettura di un valore nullo sollevavano un TypeError.
Una delle caratteristiche più potenti dell'ORM è il Lazy Loading. Quando carichi un'entità, le sue relazioni (es. $post->author) non vengono caricate immediatamente. L'ORM carica l'oggetto User correlato solo nel momento esatto in cui provi ad accedervi (es. echo $post->author->getUsername();). Questo ottimizza drasticamente le performance, evitando query inutili.
Da un'entità referenziata (es. User), puoi accedere a tutte le entità che la referenziano (es. tutti i Post di quell'utente) tramite una "proprietà magica". La convenzione è nomeEntitàDipendenteCollection.
$user = $userModel->getEntityById(1);
// Accede a una collezione di tutti i post di questo utente.
// La query viene eseguita solo in questo momento.
$userPosts = $user->postCollection;
foreach ($userPosts as $post) {
echo $post->getTitle();
}
Esistono anche metodi magici per manipolare queste collezioni: set...(), add...(), e count...().
Un modello è la porta d'accesso al database per una specifica entità. Deve estendere una delle classi base (BaseModel, DependentModel, SelfDependentModel) e specificare a quale entità è associato.
namespace MyModule\App\Models;
use SismaFramework\Orm\BaseClasses\BaseModel;
use MyModule\App\Entities\Post;
class PostModel extends BaseModel
{
protected static function getEntityName(): string
{
return Post::class;
}
// Usato per le ricerche testuali
protected function appendSearchCondition(Query &$query, string $searchKey, array &$bindValues, array &$bindTypes): void
{
$query->where('title LIKE :searchKey OR content LIKE :searchKey');
$bindValues[':searchKey'] = '%' . $searchKey . '%';
}
}
BaseModel)Ogni modello eredita un set di metodi standard:
getEntityById(int $id): Trova un'entità tramite il suo ID.getEntityCollection(?string $searchKey, ?array $order, ?int $offset, ?int $limit): Recupera una collezione di entità, con opzioni di filtro, ordinamento e paginazione.countEntityCollection(?string $searchKey): Conta le entità, opzionalmente filtrate.deleteEntityById(int $id): Elimina un'entità dal suo ID.Esempio di utilizzo in un Controller:
use MyModule\App\Models\PostModel;
// ... in un'action del controller ...
$postModel = new PostModel($this->dataMapper);
// Trova il post con ID 42
$post = $postModel->getEntityById(42);
// Trova tutti i post che contengono "SismaFramework", ordinati per data
$posts = $postModel->getEntityCollection('SismaFramework', ['publicationDate' => 'DESC']);
DependentModel)
Se un modello estende DependentModel, ottiene metodi per interrogare il database basandosi sulle relazioni.
getBy{PropertyName}(...) (metodo magico, vedi sezione successiva): Trova entità che corrispondono a una o più entità referenziate.getOtherEntityCollectionByEntity(BaseEntity $excludedEntity, string $propertyName, ?BaseEntity $baseEntity = null): Trova le entità collegate a $baseEntity tramite la proprietà $propertyName, escludendo $excludedEntity. Con $baseEntity a null trova le entità in cui la relazione è assente (v12.9.1+; fino alla 12.9.0 il parametro non ammetteva null e la query generata non era valida, vedi CHANGELOG 12.9.1).Esempio: Trova tutti i post di un certo utente.
use MyModule\App\Models\PostModel; // Deve estendere DependentModel
$postModel = new PostModel($this->dataMapper);
$user = $userModel->getEntityById(1);
// Trova tutti i post dove 'author' è l'utente con ID 1
$userPosts = $postModel->getByAuthor($user);
// Gli altri post dello stesso autore, escluso quello corrente
$otherPosts = $postModel->getOtherEntityCollectionByEntity($post, 'author', $user); // WHERE id <> ? AND author_id = ?
// Gli altri post senza autore, escluso quello corrente
$otherOrphanPosts = $postModel->getOtherEntityCollectionByEntity($post, 'author'); // WHERE id <> ? AND author_id IS NULL
A partire dalla versione 10.1.0, l'ORM supporta query dinamiche su tutte le proprietà delle entità, non solo sulle relazioni. Questo sistema di metaprogrammazione genera automaticamente query type-safe basandosi sui nomi dei metodi chiamati.
La sintassi generale è: get|count|delete + By + NomeProprietà [+ And + AltraProprietà]
Puoi interrogare il database utilizzando qualsiasi proprietà dell'entità:
// Query su entità referenziate (come prima)
$userPosts = $postModel->getByAuthor($user);
$postCount = $postModel->countByAuthor($user);
$postModel->deleteByAuthor($user);
// Query su tipi builtin (int, string, float, bool) - NUOVO in 10.1.0
$activeUsers = $userModel->getByStatus('active');
$expensiveProducts = $productModel->getByPrice(99.99);
$publishedPosts = $postModel->getByPublished(true);
$totalActiveUsers = $userModel->countByActive(true);
// Query con enum PHP 8.1+
$activePosts = $postModel->getByStatus(PostStatus::Published);
$draftCount = $postModel->countByStatus(PostStatus::Draft);
// Query con tipi custom (SismaDateTime, SismaDate, SismaTime)
$todayPosts = $postModel->getByPublicationDate(new SismaDate('2025-01-15'));
$recentPosts = $postModel->getByCreatedAt(new SismaDateTime('2025-01-01 00:00:00'));
Puoi combinare più proprietà con operatore AND:
// Query con due proprietà
$products = $productModel->getByNameAndCategory('iPhone', $electronicsCategory);
$users = $userModel->getByStatusAndRole('active', 'admin');
// Query con tre o più proprietà
$articles = $articleModel->getByAuthorAndCategoryAndStatus($user, $category, ArticleStatus::Published);
Le query dinamiche gestiscono correttamente i valori null su proprietà nullable:
// Trova entità con proprietà nullable = NULL
// Su una foreign key (proprietà tipizzata come ReferencedEntity) la colonna
// generata ha sempre il suffisso Id, anche con valore null (v12.3.0+; prima
// di questa correzione il valore null generava erroneamente la colonna senza
// suffisso — vedi CHANGELOG 12.2.1):
$orphanPosts = $postModel->getByParentPost(null); // WHERE parent_post_id IS NULL
// Trova utenti senza avatar
$usersWithoutAvatar = $userModel->getByAvatarUrl(null); // WHERE avatar_url IS NULL
⚠️ Nota: Passare null a una proprietà non-nullable lancerà un'eccezione InvalidArgumentException.
I metodi magici supportano gli stessi parametri opzionali dei metodi standard:
// Sintassi completa per getBy...()
$posts = $postModel->getByAuthor(
$author, // Valore della proprietà
'keyword', // searchKey (opzionale)
['publicationDate' => 'DESC'], // order (opzionale)
0, // offset (opzionale)
20 // limit (opzionale)
);
// Con paginazione
$page2Posts = $postModel->getByCategory($category, null, null, 20, 10);
Per modelli auto-referenziati, le query dinamiche supportano anche il parametro parent:
$subCategories = $categoryModel->getByParentAndActive($parentCategory, true);
$childCount = $categoryModel->countByParentAndStatus($parentCategory, 'active');
getOtherEntityCollectionByParent(BaseEntity $excludedEntity, ?BaseEntity $parentEntity = null, ?array $order = null) restituisce le entità sorelle di $excludedEntity, cioè quelle con lo stesso padre esclusa l'entità stessa; con $parentEntity a null restituisce le altre entità radice. Fino alla 12.9.0 la condizione di esclusione veniva generata sulla colonna inesistente id_id (vedi CHANGELOG 12.9.1):
$siblingCategories = $categoryModel->getOtherEntityCollectionByParent($category, $parentCategory); // WHERE id <> ? AND parent_category_id = ?
$otherRootCategories = $categoryModel->getOtherEntityCollectionByParent($rootCategory); // WHERE id <> ? AND parent_category_id IS NULL
Il sistema utilizza Reflection per validare automaticamente i tipi:
null solo su proprietà nullableUser, SismaDateTime, enum, etc.)InvalidArgumentException in caso di tipo non valido// ✅ Corretto - tipo compatibile
$posts = $postModel->getByViewCount(1000); // int
// ❌ Errore - tipo non compatibile
$posts = $postModel->getByViewCount('mille'); // InvalidArgumentException
I seguenti metodi erano deprecati dalla versione 10.1.0 e sono stati rimossi nella v12.0.0. Utilizza le query dinamiche:
DependentModel:
getEntityCollectionByEntity()~~ → getBy{PropertyName}()countEntityCollectionByEntity()~~ → countBy{PropertyName}()deleteEntityCollectionByEntity()~~ → deleteBy{PropertyName}()SelfDependentModel:
getEntityCollectionByParentAndEntity()~~ → getByParentAnd{PropertyName}()countEntityCollectionByParentAndEntity()~~ → countByParentAnd{PropertyName}()deleteEntityCollectionByParentAndEntity()~~ → deleteByParentAnd{PropertyName}()Esempio di migrazione:
// VECCHIO (rimosso in v12.0.0)
$posts = $postModel->getEntityCollectionByEntity(['author' => $user, 'category' => $category]);
// NUOVO (consigliato)
$posts = $postModel->getByAuthorAndCategory($user, $category);
Per le query che vanno oltre i metodi standard, puoi creare metodi personalizzati all'interno della tua classe Model utilizzando l'oggetto Query.
Questo ti permette di costruire istruzioni SQL complesse in modo programmatico.
Supponiamo di voler trovare tutti i post che contengono una certa parola nel titolo e che sono in uno stato specifico (es. Published).
MyModule/App/Models/PostModel.php
namespace MyModule\App\Models;
use SismaFramework\Orm\BaseClasses\DependentModel; // o BaseModel
use SismaFramework\Orm\HelperClasses\Query;
use SismaFramework\Orm\HelperClasses\SismaCollection;
use SismaFramework\Orm\Enumerations\ComparisonOperator;
use SismaFramework\Orm\Enumerations\Placeholder;
use MyModule\App\Enumerations\PostStatus;
class PostModel extends DependentModel
{
// ... getEntityName() e appendSearchCondition() ...
public function findPublishedByTitle(string $titleKeyword): SismaCollection
{
// 1. Inizializza la query usando il metodo del modello
$query = $this->initQuery();
// 2. Costruisci la clausola WHERE
$query->setWhere()
->appendOpenBlock()
->appendCondition('title', ComparisonOperator::like, Placeholder::placeholder)
->appendCloseBlock()
->appendAnd()
->appendCondition('status', ComparisonOperator::equal, Placeholder::placeholder);
// 3. Imposta l'ordinamento
$query->setOrderBy(['publicationDate' => 'DESC']);
// 4. Prepara i valori e i tipi per il binding
$bindValues = [
'%' . $titleKeyword . '%',
PostStatus::Published
];
// I bindTypes sono opzionali - il sistema parsa automaticamente i valori
$bindTypes = [];
// 5. Esegui la query e restituisci la collezione
$query->close();
return $this->dataMapper->find($this->entityName, $query, $bindValues, $bindTypes);
}
}
A partire dalla versione 10.1.0, l'ORM supporta nativamente le funzioni di aggregazione SQL (AVG, MAX, MIN, SUM) tramite la classe Query. Queste funzioni permettono di eseguire calcoli statistici direttamente nel database.
setAVG() - Calcola la media di una colonnasetMax() - Trova il valore massimosetMin() - Trova il valore minimosetSum() - Calcola la somma totale$query->setAVG(
string|Query $columnOrSubquery, // Colonna o subquery da aggregare
?string $columnAlias = null, // Alias per il risultato (opzionale)
bool $distinct = false, // Applica DISTINCT (opzionale)
bool $append = false // Aggiungi alle colonne esistenti (opzionale)
);
Gli stessi parametri si applicano a setMax(), setMin() e setSum().
// Calcolare la media dei prezzi
public function getAveragePrice(): float
{
$query = $this->initQuery();
$query->setAVG('price', 'average_price');
$query->close();
$result = $this->dataMapper->find($this->entityName, $query);
return $result[0]->averagePrice ?? 0.0;
}
// Trovare il prodotto più costoso
public function getMaxPrice(): float
{
$query = $this->initQuery();
$query->setMax('price', 'max_price');
$query->close();
$result = $this->dataMapper->find($this->entityName, $query);
return $result[0]->maxPrice ?? 0.0;
}
// Somma totale delle vendite
public function getTotalSales(): float
{
$query = $this->initQuery();
$query->setSum('amount', 'total_sales');
$query->close();
$result = $this->dataMapper->find($this->entityName, $query);
return $result[0]->totalSales ?? 0.0;
}
Utilizza il parametro $distinct per aggregare solo valori unici:
// Somma dei prezzi distinti (ignora duplicati)
public function getSumOfDistinctPrices(): float
{
$query = $this->initQuery();
$query->setSum('price', 'total_distinct_prices', distinct: true);
$query->close();
$result = $this->dataMapper->find($this->entityName, $query);
return $result[0]->totalDistinctPrices ?? 0.0;
}
// Media delle valutazioni distinte
public function getAverageDistinctRating(): float
{
$query = $this->initQuery();
$query->setAVG('rating', 'avg_rating', distinct: true);
$query->close();
$result = $this->dataMapper->find($this->entityName, $query);
return $result[0]->avgRating ?? 0.0;
}
$append
Il parametro $append permette di calcolare più aggregazioni nella stessa query:
// Statistiche complete sui prezzi
public function getPriceStatistics(): object
{
$query = $this->initQuery();
// Prima aggregazione (sostituisce le colonne)
$query->setMin('price', 'min_price');
// Aggregazioni successive (aggiungono alle colonne)
$query->setMax('price', 'max_price', append: true);
$query->setAVG('price', 'avg_price', append: true);
$query->setSum('price', 'total_price', append: true);
$query->close();
$result = $this->dataMapper->find($this->entityName, $query);
return $result[0] ?? (object)[
'minPrice' => 0,
'maxPrice' => 0,
'avgPrice' => 0,
'totalPrice' => 0
];
}
⚠️ Nota: Senza $append, ogni chiamata sostituisce le colonne precedenti. Con $append = true, le aggregazioni vengono aggiunte.
Combina aggregazioni con filtri per calcoli condizionali:
// Media dei prezzi solo per prodotti attivi
public function getAveragePriceForActiveProducts(): float
{
$query = $this->initQuery();
$query->setAVG('price', 'avg_active_price');
$query->setWhere()
->appendCondition('status', ComparisonOperator::equal, Placeholder::placeholder);
$query->close();
$result = $this->dataMapper->find(
$this->entityName,
$query,
[ProductStatus::Active]
);
return $result[0]->avgActivePrice ?? 0.0;
}
Combina aggregazioni con raggruppamento per analisi dettagliate:
// Somma vendite per categoria
public function getSalesByCategory(): SismaCollection
{
$query = $this->initQuery();
$query->setColumns(['category_id']);
$query->setSum('amount', 'total_sales', append: true);
$query->setGroupBy(['category_id']);
$query->setOrderBy(['total_sales' => 'DESC']);
$query->close();
return $this->dataMapper->find($this->entityName, $query);
}
Le funzioni di aggregazione supportano anche subquery come argomento:
// Media del numero di commenti per post
public function getAverageCommentsPerPost(): float
{
// Subquery per contare commenti per post
$commentCountQuery = (new CommentModel($this->dataMapper))->initQuery();
$commentCountQuery->setColumns(['COUNT(*)'])
->setWhere()
->appendCondition('post_id', ComparisonOperator::equal, 'post.id');
$commentCountQuery->close();
// Aggregazione sulla subquery
$query = $this->initQuery();
$query->setAVG($commentCountQuery, 'avg_comments');
$query->close();
$result = $this->dataMapper->find($this->entityName, $query);
return $result[0]->avgComments ?? 0.0;
}
Dashboard con statistiche prodotti:
public function getDashboardStats(): object
{
$query = $this->initQuery();
// Multiple aggregazioni
$query->setSum('quantity', 'total_quantity')
->setAVG('price', 'average_price', append: true)
->setMin('price', 'cheapest_price', append: true)
->setMax('price', 'most_expensive_price', append: true);
// Solo prodotti in stock
$query->setWhere()
->appendCondition('in_stock', ComparisonOperator::equal, Placeholder::placeholder);
$query->close();
$result = $this->dataMapper->find($this->entityName, $query, [true]);
return $result[0] ?? (object)[];
}
Report vendite mensili con aggregazioni:
public function getMonthlySalesReport(): SismaCollection
{
$query = $this->initQuery();
// Seleziona anno e mese
$query->setColumns(['YEAR(sale_date) as year', 'MONTH(sale_date) as month']);
// Aggregazioni multiple
$query->setSum('amount', 'total_sales', append: true)
->setAVG('amount', 'avg_sale', append: true)
->setMax('amount', 'max_sale', append: true);
// Raggruppa per anno e mese
$query->setGroupBy(['YEAR(sale_date)', 'MONTH(sale_date)']);
$query->setOrderBy(['year' => 'DESC', 'month' => 'DESC']);
$query->close();
return $this->dataMapper->find($this->entityName, $query);
}
L'ORM supporta l'uso di subquery sia come colonne che come condizioni nella clausola WHERE. Le subquery sono query annidate che possono essere utilizzate per creare query complesse.
Puoi utilizzare una subquery come colonna nel SELECT:
public function getPostsWithCommentCount(): SismaCollection
{
$query = $this->initQuery();
// Crea la subquery per contare i commenti
$commentCountQuery = (new CommentModel($this->dataMapper))->initQuery();
$commentCountQuery->setColumns(['COUNT(*)'])
->setWhere()
->appendCondition('post_id', ComparisonOperator::equal, 'post.id');
$commentCountQuery->close();
// Usa la subquery come colonna
$query->setSubqueryColumn($commentCountQuery, 'comment_count', true);
$query->close();
return $this->dataMapper->find($this->entityName, $query);
}
Puoi utilizzare subquery nelle condizioni WHERE per filtri complessi:
public function getPopularPosts(): SismaCollection
{
$query = $this->initQuery();
// Subquery per trovare la media dei view_count
$avgViewsQuery = $this->initQuery();
$avgViewsQuery->setColumns(['AVG(view_count)']);
$avgViewsQuery->close();
// Condizione con subquery
$query->setWhere()
->appendSubqueryCondition($avgViewsQuery, ComparisonOperator::greaterThan);
$query->close();
return $this->dataMapper->find($this->entityName, $query);
}
public function getPostsWithComments(): SismaCollection
{
$query = $this->initQuery();
// Subquery per verificare esistenza commenti
$existsQuery = (new CommentModel($this->dataMapper))->initQuery();
$existsQuery->setColumns(['1'])
->setWhere()
->appendCondition('post_id', ComparisonOperator::equal, 'post.id');
$existsQuery->close();
$query->setWhere()
->appendSubqueryCondition($existsQuery, ComparisonOperator::exists);
$query->close();
return $this->dataMapper->find($this->entityName, $query);
}
Indice | Precedente: Gestione degli Asset Statici | Successivo: Funzionalità Avanzate dell'ORM
Se hai trovato errori o vuoi suggerire miglioramenti, apri una issue su GitHub.
Report Issue Edit on GitHub