@@ -78,8 +78,53 @@ class Breakfast_Model extends Model {
7878}
7979```
8080
81+ ### A ReadOnly model
82+
83+ This is a model whose intent is to only read and store data. The Read operations should - in most cases - be deferred to
84+ a repository class, but the model should provide a simple interface for interacting with the repository. You can create
85+ ReadOnly model by implementing the ` Contracts\ModelReadOnly ` contract.
86+
87+ ``` php
88+ namespace Boomshakalaka\Whatever;
89+
90+ use Boomshakalaka\StellarWP\Models\Contracts;
91+ use Boomshakalaka\StellarWP\Models\Model;
92+ use Boomshakalaka\StellarWP\Models\ModelQueryBuilder;
93+
94+ class Breakfast_Model extends Model implements Contracts\ModelReadOnly {
95+ /**
96+ * @inheritDoc
97+ */
98+ protected $properties = [
99+ 'id' => 'int',
100+ 'name' => 'string',
101+ 'price' => 'float',
102+ 'num_eggs' => 'int',
103+ 'has_bacon' => 'bool',
104+ ];
105+
106+ /**
107+ * @inheritDoc
108+ */
109+ public static function find( $id ) : Model {
110+ return App::get( Repository::class )->get_by_id( $id );
111+ }
112+
113+ /**
114+ * @inheritDoc
115+ */
116+ public static function query() : ModelQueryBuilder {
117+ return App::get( Repository::class )->prepare_query();
118+ }
119+ }
120+ ```
121+
81122### A CRUD model
82123
124+ This is a model that includes CRUD operations. Ideally, the actual CRUD operations should be deferred to and handled by
125+ a repository class, but the model should provide a simple interface for interacting with the repository. We get a CRUD
126+ model by implementing the ` Contracts\ModelCrud ` contract.
127+
83128``` php
84129namespace Boomshakalaka\Whatever;
85130
@@ -133,37 +178,274 @@ class Breakfast_Model extends Model implements Contracts\ModelCrud {
133178 * @inheritDoc
134179 */
135180 public static function query() : ModelQueryBuilder {
136- return App::get( Repository::class )->prepare_query ();
181+ return App::get( Repository::class )->prepareQuery ();
137182 }
138183}
139184```
140185
141- ## Interacting with a model
186+ ## Data Transfer Objects
142187
143- ## Data transfer objects
188+ Data Transfer Objects (DTOs) are classes that help with the translation of database query results (or other sources of data)
189+ into models. DTOs are not required for using this library, but they are recommended. Using these objects helps you be more
190+ deliberate with your query usage and allows your models and repositories well with the ` ModelQueryBuilder ` .
191+
192+ Here's an example of a DTO for breakfasts:
193+
194+ ``` php
195+ namespace Boomshakalaka\Whatever;
196+
197+ use Boomshakalaka\Whatever\StellarWP\Models\DataTransferObject;
198+ use Boomshakalaka\Whatever\Breakfast_Model;
199+
200+ class Breakfast_DTO extends DataTransferObject {
201+ /**
202+ * Breakfast ID.
203+ *
204+ * @var int
205+ */
206+ public int $id;
207+
208+ /**
209+ * Breakfast name.
210+ *
211+ * @var string
212+ */
213+ public string $name;
214+
215+ /**
216+ * Breakfast price.
217+ *
218+ * @var float
219+ */
220+ public float $price;
221+
222+ /**
223+ * Number of eggs in the breakfast.
224+ *
225+ * @var int
226+ */
227+ public int $num_eggs;
228+
229+ /**
230+ * Whether or not the breakfast has bacon.
231+ *
232+ * @var bool
233+ */
234+ public bool $has_bacon;
235+
236+ /**
237+ * Builds a new DTO from an object.
238+ *
239+ * @since TBD
240+ *
241+ * @param object $object The object to build the DTO from.
242+ *
243+ * @return Breakfast_DTO The DTO instance.
244+ */
245+ public static function fromObject( $object ): self {
246+ $self = new self();
247+
248+ $self->id = $object->id;
249+ $self->name = $object->name;
250+ $self->price = $object->price;
251+ $self->num_eggs = $object->num_eggs;
252+ $self->has_bacon = (bool) $object->has_bacon;
253+
254+ return $self;
255+ }
256+
257+ /**
258+ * Builds a model instance from the DTO.
259+ *
260+ * @since TBD
261+ *
262+ * @return Breakfast_Model The model instance.
263+ */
264+ public function toModel(): Breakfast_Model {
265+ $attributes = get_object_vars( $this );
266+
267+ return new Breakfast_Model( $attributes );
268+ }
269+ }
270+ ```
271+
272+ ## Repositories
273+
274+ Repositories are classes that fetch from and interact with the database. Ideally, repositories would be used to
275+ query the database in different ways and return corresponding models. With this library, we provide
276+ ` Deletable ` , ` Insertable ` , and ` Updatable ` contracts that can be used to indicate what operations a repository provides.
277+
278+ You may be wondering why there isn't a ` Findable ` or ` Readable ` contract (or similar). That's because the fetching needs
279+ of a repository varies with the usecase. However, in the ` Repository ` abstract class, there is an abstract ` prepareQuery() `
280+ method. This method should return a ` ModelQueryBuilder ` instance that can be used to fetch data from the database.
281+
282+ ``` php
283+ namespace Boomshakalaka\Whatever;
284+
285+ use Boomshakalaka\StellarWP\Models\Contracts\Model;
286+ use Boomshakalaka\StellarWP\Models\ModelQueryBuilder;
287+ use Boomshakalaka\StellarWP\Repositories\Repository;
288+ use Boomshakalaka\StellarWP\Repositories\Contracts;
289+ use Boomshakalaka\Whatever\Breakfast_Model;
290+ use Boomshakalaka\Whatever\Breakfast as Table;
291+
292+ class Breakfast_Repository extends Repository implements Contracts\Deletable, Contracts\Insertable, Contracts\Updatable {
293+ /**
294+ * {@inheritDoc}
295+ */
296+ public function delete( Model $model ): bool {
297+ return (bool) DB::delete( Table::table_name(), [ 'id' => $model->id ], [ '%d' ] );
298+ }
299+
300+ /**
301+ * {@inheritDoc}
302+ */
303+ public function insert( Model $model ): Breakfast_Model {
304+ DB::insert( Table::table_name(), [
305+ 'name' => $model->name,
306+ 'price' => $model->price,
307+ 'num_eggs' => $model->num_eggs,
308+ 'has_bacon' => (int) $model->has_bacon,
309+ ], [
310+ '%s',
311+ '%s',
312+ '%d',
313+ '%d',
314+ ] );
315+
316+ $model->id = DB::last_insert_id();
317+
318+ return $model;
319+ }
320+
321+ /**
322+ * {@inheritDoc}
323+ */
324+ function prepareQuery(): ModelQueryBuilder {
325+ $builder = new ModelQueryBuilder( Breakfast_Model::class );
326+
327+ return $builder->from( Table::table_name( false ) );
328+ }
329+
330+ /**
331+ * {@inheritDoc}
332+ */
333+ public function update( Model $model ): Model {
334+ DB::update( Table::table_name(), [
335+ 'name' => $model->name,
336+ 'price' => $model->price,
337+ 'num_eggs' => $model->num_eggs,
338+ 'has_bacon' => (int) $model->has_bacon,
339+ ], [ 'id' => $model->id ], [
340+ '%s',
341+ '%s',
342+ '%d',
343+ '%d',
344+ ], [ '%d' ] );
345+
346+ return $model;
347+ }
348+
349+ /**
350+ * Finds a Breakfast by its ID.
351+ *
352+ * @since TBD
353+ *
354+ * @param int $id The ID of the Breakfast to find.
355+ *
356+ * @return Breakfast_Model|null The Breakfast model instance, or null if not found.
357+ */
358+ public function find_by_id( int $id ): ?Breakfast_Model {
359+ return $this->prepareQuery()->where( 'id', $id )->get();
360+ }
361+ }
362+ ```
363+
364+ ### Interacting with the Repository
365+
366+ ### Querying
367+
368+ ``` php
369+ $breakfast = App::get( Breakfast_Repository::class )->find_by_id( 1 );
370+
371+ // Or, we can fetch via the model, which defers to the repository.
372+ $breakfast = Breakfast_Model::find( 1 );
373+ ```
374+
375+ ### Inserting
376+
377+ ``` php
378+ $breakfast = new Breakfast_Model( [
379+ 'name' => 'Bacon and Eggs',
380+ 'price' => 5.99,
381+ 'num_eggs' => 2,
382+ 'has_bacon' => true,
383+ ] );
384+
385+ $breakfast->save();
386+ ```
387+
388+ ### Updating
389+
390+ ``` php
391+ $breakfast = Breakfast_Model::find( 1 );
392+ $breakfast->setAttribute( 'price', 6.99 );
393+ $breakfast->save();
394+ ```
395+
396+ ### Deleting
397+
398+ ``` php
399+ $breakfast = Breakfast_Model::find( 1 );
400+ $breakfast->delete();
401+ ```
144402
145403## Classes of note
146404
147405### ` Model `
148406
407+ This is an abstract class to extend for your models.
408+
149409### ` ModelFactory `
150410
411+ This is an abstract class to extend for creating model factories.
412+
151413### ` ModelQueryBuilder `
152414
415+ This class extends the [ ` stellarwp/db ` ] ( https://github.com/stellarwp/db ) ` QueryBuilder ` class so that it returns
416+ model instances rather than arrays or ` stdClass ` instances. Using this requires models that implement the ` ModelFromQueryBuilderObject `
417+ interface.
418+
153419### ` DataTransferObject `
154420
421+ This is an abstract class to extend for your DTOs.
422+
155423### ` Repositories\Repository `
156424
425+ This is an abstract class to extend for your repositories.
426+
157427## Contracts of note
158428
159429### ` Contracts\ModelCrud `
160430
431+ Provides definitions of methods for CRUD operations in a model.
432+
161433### ` Contracts\ModelHasFactory `
162434
435+ Provides definition for factory methods within a model.
436+
163437### ` Contracts\ModelReadOnly `
164438
439+ Provides method signatures for read operations in a model.
440+
165441### ` Repositories\Contracts\Deletable `
166442
443+ Provides method signatures for delete methods in a repository.
444+
167445### ` Repositories\Contracts\Insertable `
168446
447+ Provides method signatures for insert methods in a repository.
448+
169449### ` Repositories\Contracts\Updatable `
450+
451+ Provides method signatures for update methods in a repository.
0 commit comments