123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- <?php
- // Copyright 2019 Hackware SpA <human@hackware.cl>
- // "Hackware Web Services Core" is released under the MIT License terms.
- namespace Hawese\Core;
- use Hawese\Core\Exceptions\ModelObjectNotFoundException;
- use Hawese\Core\Exceptions\ModelValidationException;
- use Hawese\Core\Exceptions\UnknownForeignObjectException;
- use Illuminate\Contracts\Support\Arrayable;
- use Illuminate\Contracts\Support\Jsonable;
- use Illuminate\Database\Query\Builder;
- use Illuminate\Support\Carbon;
- use Illuminate\Support\Collection;
- use Illuminate\Validation\ValidationException;
- use ArrayAccess;
- use Exception;
- use JsonSerializable;
- /**
- * Base class for CRUD database models.
- *
- * You always need to define the static `$table` and `$attributes` props.
- *
- * - Supports find($pk), insert(), update() and delete() operations.
- * - select() will return a Laravel's Query Builder object.
- * - processCollection() will process each element of a Query Builder produced
- * resultset with this class and allow you to `appendForeignObjects`.
- * - If you use the `created_at`, `updated_at` and/or `deleted_at` `$attributes`
- * in your class declaration, it will automagically fill those fields.
- */
- abstract class TableModel implements
- ArrayAccess,
- JsonSerializable,
- Arrayable,
- Jsonable
- {
- public static $table;
- public static $attributes = []; // ['prop1' => ['validations'], ...]
- protected $appends = []; // Append JSONable attributes at runtime
- protected $hidden = ['deleted_at']; // Hide from serialization
- public static $primary_key = 'id';
- protected static $incrementing = true;
- public static $foreign_keys = []; // ['foreign_key' => Class::class, ...]
- protected $data = []; // Where $attributes values are saved
- protected $current_primary_key;
- /**
- * Bootstrapping
- * =============
- */
- /**
- * @param (array|object)[] $data Associative data with object attributes.
- */
- public function __construct($data = null)
- {
- $this->data = array_fill_keys(static::attributes(), null);
- if (isset($data)) {
- foreach ($data as $key => $value) {
- $this->{$key} = $value;
- }
- }
- $this->current_primary_key = $this->{static::$primary_key};
- }
- /**
- * Magic accesors.
- *
- * Custom getters must be camelCased and start with `get` followed by
- * `AttributeName`.
- *
- * ```php
- * protected function getCustomAttribute()
- * {
- * return $this->data['custom_attribute'];
- * }
- * ```
- */
- public function __get(string $name)
- {
- $getter = 'get' . self::snakeToPascalCase($name);
- if (method_exists(static::class, $getter)) {
- return $this->{$getter}();
- }
- if (in_array($name, $this->instanceAttributes())) {
- return $this->data[$name] ?? null;
- }
- $trace = debug_backtrace();
- trigger_error(
- 'Undefined property via __get(): ' . $name .
- ' in ' . $trace[0]['file'] .
- ' on line ' . $trace[0]['line'],
- E_USER_NOTICE
- );
- return null;
- }
- /**
- * Magic mutators.
- *
- * Custom setters must be camelCased and start with `set` followed by
- * `AttributeName`, `$new_value` must be assigned to the local
- * `$data['attribute_name']` store.
- *
- * ```php
- * protected function setCustomAttribute($new_value)
- * {
- * $this->data['custom_attribute'] = $new_value;
- * }
- * ```
- */
- public function __set(string $name, $value)
- {
- $setter = 'set' . self::snakeToPascalCase($name);
- if (method_exists(static::class, $setter)) {
- return $this->{$setter}($value);
- }
- if (in_array($name, $this->instanceAttributes())) {
- return $this->data[$name] = $value;
- }
- $trace = debug_backtrace();
- trigger_error(
- 'Undefined property via __set(): ' . $name .
- ' in ' . $trace[0]['file'] .
- ' on line ' . $trace[0]['line'],
- E_USER_NOTICE
- );
- }
- public function __isset(string $name)
- {
- return isset($this->data[$name]);
- }
- /**
- * Implements ArrayAccess.
- *
- * So you can ->pluck() on a collection or do these kind of nice things
- */
- public function offsetExists($offset)
- {
- return !!$this->{$offset};
- }
- public function offsetGet($offset)
- {
- return $this->{$offset};
- }
-
- public function offsetSet($offset, $value)
- {
- $this->{$offset} = $value;
- }
-
- public function offsetUnset($offset)
- {
- unset($this->data[$offset]);
- }
- /**
- * Implements toArray
- */
- public function toArray()
- {
- $data = [];
- // Process getters
- foreach ($this->instanceAttributes() as $attribute) {
- if ($this->{$attribute} instanceof Carbon) {
- // for $(crea|upda|dele)ted_at
- $data[$attribute] = $this->{$attribute}->format('c');
- } elseif ($this->{$attribute} instanceof Arrayable) {
- // for $foreign_keys
- $data[$attribute] = $this->{$attribute}->toArray();
- } else {
- $data[$attribute] = $this->{$attribute};
- }
- }
- // Forget hidden attributes
- // Not in toJson(), since in that context is not possible to determine
- // foreign object's hidden attributes.
- foreach ($this->hidden as $attribute) {
- unset($data[$attribute]);
- }
- return $data;
- }
- /**
- * Implements JsonSerializable
- */
- public function jsonSerialize()
- {
- return $this->toArray();
- }
- /**
- * Implements Jsonable
- */
- public function toJson($options = 0)
- {
- return json_encode($this->jsonSerialize(), $options);
- }
- /**
- * Automagically use carbon on $(crea|upda|dele)ted_at
- */
- protected function dateSetter($attribute, $date) : void
- {
- if ($date instanceof Carbon || empty($date)) {
- $this->data[$attribute] = $date;
- } else {
- $this->data[$attribute] = new Carbon($date);
- }
- }
- public function setCreatedAt($value) : void
- {
- $this->dateSetter('created_at', $value);
- }
-
- public function setUpdatedAt($value) : void
- {
- $this->dateSetter('updated_at', $value);
- }
-
- public function setDeletedAt($value) : void
- {
- $this->dateSetter('deleted_at', $value);
- }
- /**
- * Return previously loaded relationship or load it now
- */
- protected function foreignObjectGetter($attribute)
- {
- if (array_key_exists($attribute, $this->data)) {
- return $this->data[$attribute];
- }
- $foreign_key = static::guessFK($attribute);
- return static::$foreign_keys[$foreign_key]::find($this->{$foreign_key});
- }
- /**
- * Main functionality
- * ==================
- */
- public static function select(?array $attributes = null) : Builder
- {
- if (!$attributes) {
- $attributes = static::attributes();
- }
- return app('db')
- ->table(static::$table)
- ->select(
- preg_filter('/^/', static::$table . '.', $attributes)
- );
- }
- /**
- * @params string $value checks against this value for equality.
- * @params string|array $fields field to compare, defaults to
- * static::$primary_key, if array is provided will try with any of
- * the provided fields.
- */
- public static function find(string $value, $fields = null) : self
- {
- $fields = $fields ?? static::$primary_key;
- if (!is_array($fields)) {
- $fields = [$fields];
- }
- $query = 'SELECT * FROM ' . static::$table . ' WHERE ';
- $query_fields = $fields;
- array_walk($query_fields, function (&$field) {
- $field = static::$table . ".$field = ? ";
- });
- $query .= implode(' OR ', $query_fields);
- if (in_array('deleted_at', static::attributes())) {
- $query .= 'AND deleted_at IS NULL';
- }
- $row = app('db')->selectOne(
- $query,
- array_fill(0, count($fields), $value)
- );
- if ($row) {
- return new static($row);
- }
- throw new ModelObjectNotFoundException(
- static::class,
- implode(' or ', $fields),
- $value
- );
- }
- /**
- * @return mixed primary key value
- */
- public function insert()
- {
- $this->validate();
- $fields_to_insert = static::attributes();
- if (in_array('created_at', $this->instanceAttributes())) {
- $this->created_at = new Carbon();
- }
- $query = 'INSERT INTO ' . static::$table . ' (';
- $query .= implode(
- ',',
- preg_filter('/^/', static::$table . '.', $fields_to_insert)
- );
- $query .= ') VALUES (';
- $query .= trim(str_repeat('?,', count($fields_to_insert)), ',');
- $query .= ')';
- app('db')->insert(
- $query,
- array_map(
- function ($field) {
- return $this->{$field};
- },
- $fields_to_insert
- )
- );
- if (static::$incrementing) {
- $this->{static::$primary_key} = app('db')->getPdo()->lastInsertId();
- }
- return $this->{static::$primary_key};
- }
- public function update($fields = []) : bool
- {
- $this->validate();
- if (empty($fields)) {
- $fields = array_filter(
- static::attributes(),
- function ($attribute) {
- return isset($this->data[$attribute]);
- }
- );
- }
- if (in_array('updated_at', $this->instanceAttributes())) {
- if (!in_array('updated_at', $fields)) {
- array_push($fields, 'updated_at');
- }
- $this->updated_at = new Carbon();
- }
-
- $query = 'UPDATE ' . static::$table . ' SET';
- $query .= trim(array_reduce(
- $fields,
- function ($carry, $item) {
- return $carry . ' ' . static::$table . ".$item=?,";
- },
- ''
- ), ',');
- $query .= ' WHERE ' . static::$primary_key . ' = ?';
- $operation = app('db')->update(
- $query,
- array_merge(
- array_map(
- function ($field) {
- return $this->{$field};
- },
- $fields
- ),
- [$this->current_primary_key]
- )
- );
- if ($operation) {
- $this->current_primary_key = $this->{static::$primary_key};
- }
- return $operation;
- }
- public function delete() : bool
- {
- return static::staticDelete([$this->{static::$primary_key}]);
- }
- /**
- * Delete (potentially massively) based on primary key
- */
- public static function staticDelete(array $primary_keys): bool
- {
- $question_marks = self::commaStrRepeat('?', count($primary_keys));
- if (in_array('deleted_at', static::attributes())) {
- $deleted_at = new Carbon();
- return app('db')->update(
- 'UPDATE ' . static::$table . ' SET' .
- ' deleted_at=? WHERE ' . static::$table . '.' .
- static::$primary_key . " IN ($question_marks)",
- array_merge([Carbon::now()], $primary_keys)
- );
- }
- return app('db')->delete(
- 'DELETE FROM ' . static::$table .
- ' WHERE ' . static::$table . '.' . static::$primary_key .
- " IN ($question_marks)",
- $primary_keys
- );
- }
-
- public function validate()
- {
- try {
- return app('validator')
- ->make($this->data, static::$attributes)
- ->validate();
- } catch (ValidationException $e) {
- throw new ModelValidationException(
- static::class,
- $e->validator,
- $e->response,
- $e->errorBag
- );
- }
- }
- /**
- * Convert QueryBuilder's `get()` objects into this class' objects.
- *
- * This will allow to process getters and setters and give superpowers to
- * each object.
- *
- * @param Collection $objects Collection of plain objects.
- * @return Collection Collection of this class' objects.
- */
- public static function processCollection(
- Collection $objects
- ) : TableModelCollection {
- return new TableModelCollection($objects, static::class);
- }
- /**
- * Helpers
- * =======
- */
- public static function attributes() : array
- {
- return array_keys(static::$attributes);
- }
- public function instanceAttributes() : array
- {
- return array_merge(static::attributes(), $this->appends);
- }
- public static function foreignKeys() : array
- {
- return array_keys(static::$foreign_keys);
- }
- /*
- public static function foreignKeyObjectAttributes() : array
- {
- return array_map(function($foreign_key) {
- return preg_replace('/_[^_]*$/', '', $foreign_key);
- }, array_keys(static::$foreign_keys));
- }
- */
- public static function guessFK($attribute) : string
- {
- foreach (static::foreignKeys() as $foreign_key) {
- if (preg_match(
- '/^' . $attribute . '_[^_]+$/',
- $foreign_key,
- $matches
- )) {
- return $matches[0];
- }
- }
- throw new UnknownForeignObjectException(static::class, $attribute);
- }
- // Append attribute dynamically, so it can be get and set on a instance
- public function append($attribute) : void
- {
- array_push($this->appends, $attribute);
- }
- /**
- * General helpers
- * ===============
- *
- * This helpers might be useful out of this context too
- */
- protected static function snakeToPascalCase(string $str)
- {
- return str_replace('_', '', ucwords($str, '_'));
- }
- protected static function commaStrRepeat(string $input, int $multiplier)
- {
- if ($multiplier == 0) {
- return "";
- }
- return str_repeat("$input,", $multiplier - 1) . $input;
- }
- protected static function bcAbs(string $value)
- {
- return trim($value, '-');
- }
- }
|