From 8cd1105b111b89106f24c5b50795afb5ff28a935 Mon Sep 17 00:00:00 2001 From: Thomas Lange Date: Tue, 22 Jun 2021 01:18:02 +0200 Subject: Implement new Repository and Entity classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds new Repository and Entity classes which are better abstracted from the rest of the application. They dont know anymore about configuration options or how to parse to HTML because this is not the job for the ORM but for other parts of the application. The previous commits were a preparation for this big change. An entity now represents just a single record from a specific table of the database – nothing more. The repositories job is it to fetch or update records of the database and instantiate the entities. Another problem that was solved is the high amount of database queries that was needed before. For example, on the blogs home page first were all 10 latest post IDs fetched from the database and then another query was executed with "WHERE id = :id" for *each* single post?! ... This problem is solved with the new repository classes; they now use a single query to fetch and build the entities of the 10 latest posts. This change also solves the problem with database queries spread across the application and limits the exzessive use of try/catch blocks which were used before. The new classes make the whole code much cleaner. :) --- core/namespace/Application.php | 24 ++- core/namespace/Attribute.php | 84 ---------- core/namespace/Entity.php | 47 ++++++ core/namespace/EntityInterface.php | 8 + core/namespace/Item.php | 98 ----------- core/namespace/ItemFactory.php | 23 --- core/namespace/ItemInterface.php | 4 - core/namespace/Page/Attribute.php | 22 --- core/namespace/Page/Entity.php | 13 ++ core/namespace/Page/Exception.php | 4 - core/namespace/Page/Factory.php | 4 - core/namespace/Page/Item.php | 20 --- core/namespace/Page/Repository.php | 17 ++ core/namespace/Post/Attribute.php | 22 --- core/namespace/Post/Entity.php | 13 ++ core/namespace/Post/Exception.php | 4 - core/namespace/Post/Factory.php | 4 - core/namespace/Post/Item.php | 27 ---- core/namespace/Post/Repository.php | 17 ++ core/namespace/Repository.php | 323 +++++++++++++++++++++++++++++++++++++ core/namespace/User/Attribute.php | 24 --- core/namespace/User/Entity.php | 15 ++ core/namespace/User/Exception.php | 4 - core/namespace/User/Factory.php | 8 - core/namespace/User/Item.php | 6 - core/namespace/User/Repository.php | 7 + 26 files changed, 480 insertions(+), 362 deletions(-) delete mode 100644 core/namespace/Attribute.php create mode 100644 core/namespace/Entity.php create mode 100644 core/namespace/EntityInterface.php delete mode 100644 core/namespace/Item.php delete mode 100644 core/namespace/ItemFactory.php delete mode 100644 core/namespace/ItemInterface.php delete mode 100644 core/namespace/Page/Attribute.php create mode 100644 core/namespace/Page/Entity.php delete mode 100644 core/namespace/Page/Exception.php delete mode 100644 core/namespace/Page/Factory.php delete mode 100644 core/namespace/Page/Item.php create mode 100644 core/namespace/Page/Repository.php delete mode 100644 core/namespace/Post/Attribute.php create mode 100644 core/namespace/Post/Entity.php delete mode 100644 core/namespace/Post/Exception.php delete mode 100644 core/namespace/Post/Factory.php delete mode 100644 core/namespace/Post/Item.php create mode 100644 core/namespace/Post/Repository.php create mode 100644 core/namespace/Repository.php delete mode 100644 core/namespace/User/Attribute.php create mode 100644 core/namespace/User/Entity.php delete mode 100644 core/namespace/User/Exception.php delete mode 100644 core/namespace/User/Factory.php delete mode 100644 core/namespace/User/Item.php create mode 100644 core/namespace/User/Repository.php (limited to 'core/namespace') diff --git a/core/namespace/Application.php b/core/namespace/Application.php index f9a8196..7473e1f 100644 --- a/core/namespace/Application.php +++ b/core/namespace/Application.php @@ -6,6 +6,7 @@ class Application { #=============================================================================== private static $Database; private static $Language; + private static $repositories = []; #=============================================================================== # Configuration array @@ -67,6 +68,21 @@ class Application { return self::$Language; } + #=============================================================================== + # Return singleton repository instance + #=============================================================================== + public function getRepository(string $namespace): Repository { + $identifier = strtolower($namespace); + $repository = "$namespace\Repository"; + + if(!isset(self::$repositories[$identifier])) { + $Repository = new $repository(self::getDatabase()); + self::$repositories[$identifier] = $Repository; + } + + return self::$repositories[$identifier]; + } + #=============================================================================== # Return unique CSRF token for the current session #=============================================================================== @@ -142,15 +158,15 @@ class Application { #=============================================================================== # Return absolute URL of a specifc entity #=============================================================================== - public function getEntityURL(Item $Entity) { + public function getEntityURL(EntityInterface $Entity) { switch($class = get_class($Entity)) { - case 'Page\Item': + case 'Page\Entity': $attr = self::get('PAGE.SLUG_URLS') ? 'slug' : 'id'; return self::getPageURL($Entity->get($attr).'/'); - case 'Post\Item': + case 'Post\Entity': $attr = self::get('POST.SLUG_URLS') ? 'slug' : 'id'; return self::getPostURL($Entity->get($attr).'/'); - case 'User\Item': + case 'User\Entity': $attr = self::get('USER.SLUG_URLS') ? 'slug' : 'id'; return self::getUserURL($Entity->get($attr).'/'); default: diff --git a/core/namespace/Attribute.php b/core/namespace/Attribute.php deleted file mode 100644 index c874a57..0000000 --- a/core/namespace/Attribute.php +++ /dev/null @@ -1,84 +0,0 @@ -{$attribute} = $value; - } - - #=============================================================================== - # Get attribute - #=============================================================================== - public function get($attribute) { - return $this->{$attribute} ?? NULL; - } - - #=============================================================================== - # Get all attributes - #=============================================================================== - public function getAll($exclude = []): array { - $attributes = get_object_vars($this); - - return array_filter($attributes, function($attribute) use($exclude) { - return !in_array($attribute, $exclude); - }, ARRAY_FILTER_USE_KEY); - } - - #=============================================================================== - # Get array with not FALSE attributes - #=============================================================================== - protected function getFilteredAttributes(): array { - return array_filter(get_object_vars($this), function($value) { - return $value !== FALSE; - }); - } - - #=============================================================================== - # Insert database item - #=============================================================================== - public function insert(\Database $Database): bool { - $part[0] = ''; - $part[1] = ''; - - $attributes = $this->getFilteredAttributes(); - - foreach($attributes as $column => $value) { - $part[0] .= "{$column},"; - $part[1] .= '?,'; - } - - $part[0] = rtrim($part[0], ','); - $part[1] = rtrim($part[1], ','); - - $Statement = $Database->prepare('INSERT INTO '.static::TABLE." ({$part[0]}) VALUES ({$part[1]})"); - return $Statement->execute(array_values($attributes)); - } - - #=============================================================================== - # Update database item - #=============================================================================== - public function update(\Database $Database): bool { - $part = ''; - - $attributes = $this->getFilteredAttributes(); - - foreach($attributes as $column => $value) { - $part .= "{$column} = ?,"; - } - - $part = rtrim($part, ','); - - $Statement = $Database->prepare('UPDATE '.static::TABLE.' SET '.$part.' WHERE id = '.(int) $this->get('id')); - return $Statement->execute(array_values($attributes)); - } - - #=============================================================================== - # Delete database item - #=============================================================================== - public function delete(\Database $Database): bool { - $Statement = $Database->prepare('DELETE FROM '.static::TABLE.' WHERE id = ?'); - return $Statement->execute([$this->get('id')]); - } -} diff --git a/core/namespace/Entity.php b/core/namespace/Entity.php new file mode 100644 index 0000000..4bedd37 --- /dev/null +++ b/core/namespace/Entity.php @@ -0,0 +1,47 @@ +{$attribute} ?? NULL; + } + + #=============================================================================== + # Set attribute + #=============================================================================== + public function set(string $attribute, $value) { + return $this->{$attribute} = $value; + } + + #=============================================================================== + # Return ID + #=============================================================================== + final public function getID(): int { + return $this->id; + } + + #=============================================================================== + # Get all attributes + #=============================================================================== + public function getAll(array $exclude = []): array { + $attributes = get_object_vars($this); + + return array_filter($attributes, function($attribute) use($exclude) { + return !in_array($attribute, $exclude); + }, ARRAY_FILTER_USE_KEY); + } + + #=============================================================================== + # Get array with all non-false attributes + #=============================================================================== + public function getFilteredAttributes(): array { + return array_filter(get_object_vars($this), function($value) { + return $value !== FALSE; + }); + } +} diff --git a/core/namespace/EntityInterface.php b/core/namespace/EntityInterface.php new file mode 100644 index 0000000..8eaa089 --- /dev/null +++ b/core/namespace/EntityInterface.php @@ -0,0 +1,8 @@ +Database = $Database; - - $this->Reflection = new ReflectionObject($this); - - $attribute = "{$this->Reflection->getNamespaceName()}\\Attribute"; - $exception = "{$this->Reflection->getNamespaceName()}\\Exception"; - - #=============================================================================== - # Checking if item in database exists - #=============================================================================== - $Statement = $Database->prepare(sprintf('SELECT * FROM %s WHERE id = ?', $attribute::TABLE)); - $Statement->execute([$itemID]); - - #=============================================================================== - # Checking if retrieving data failed - #=============================================================================== - if(!$this->Attribute = $Statement->fetchObject($attribute)) { - throw new $exception(sprintf('%s\\Item with ID %s does not exist', $this->Reflection->getNamespaceName(), (int) $itemID)); - } - } - - #=============================================================================== - # Return attribute by name (short hand wrapper) - #=============================================================================== - public function get($attribute) { - return $this->Attribute->get($attribute); - } - - #=============================================================================== - # Return Attribute object - #=============================================================================== - public final function getAttribute(): Attribute { - return $this->Attribute; - } - - #=============================================================================== - # Return unique ID - #=============================================================================== - public final function getID(): int { - return $this->Attribute->get('id'); - } - - #=============================================================================== - # Return previous item ID - #=============================================================================== - public function getPrevID(): int { - $execute = 'SELECT id FROM %s WHERE time_insert < ? ORDER BY time_insert DESC LIMIT 1'; - - $attribute = "{$this->Reflection->getNamespaceName()}\\Attribute"; - $Statement = $this->Database->prepare(sprintf($execute, $attribute::TABLE)); - - if($Statement->execute([$this->Attribute->get('time_insert')])) { - return $Statement->fetchColumn(); - } - - return 0; - } - - #=============================================================================== - # Return next item ID - #=============================================================================== - public function getNextID(): int { - $execute = 'SELECT id FROM %s WHERE time_insert > ? ORDER BY time_insert ASC LIMIT 1'; - - $attribute = "{$this->Reflection->getNamespaceName()}\\Attribute"; - $Statement = $this->Database->prepare(sprintf($execute, $attribute::TABLE)); - - if($Statement->execute([$this->Attribute->get('time_insert')])) { - return $Statement->fetchColumn(); - } - - return 0; - } - - #=============================================================================== - # Return unique ID based on specific field comparison with value - #=============================================================================== - public static function getIDByField($field, $value, \Database $Database): int { - $attribute = (new ReflectionClass(get_called_class()))->getNamespaceName().'\\Attribute'; - $Statement = $Database->prepare('SELECT id FROM '.$attribute::TABLE." WHERE {$field} = ?"); - - if($Statement->execute([$value])) { - return $Statement->fetchColumn(); - } - - return 0; - } -} diff --git a/core/namespace/ItemFactory.php b/core/namespace/ItemFactory.php deleted file mode 100644 index d81ff9f..0000000 --- a/core/namespace/ItemFactory.php +++ /dev/null @@ -1,23 +0,0 @@ -getNamespaceName().'\\Item'; - $Instance = parent::storeInstance($itemID, new $Item($itemID, \Application::getDatabase())); - } - - return $Instance; - } - - #=========================================================================== - # Build instance by slug - #=========================================================================== - public static function buildBySlug($slug): Item { - $Item = (new ReflectionClass(get_called_class()))->getNamespaceName().'\\Item'; - return self::build($Item::getIDByField('slug', $slug, \Application::getDatabase())); - } -} diff --git a/core/namespace/ItemInterface.php b/core/namespace/ItemInterface.php deleted file mode 100644 index efee734..0000000 --- a/core/namespace/ItemInterface.php +++ /dev/null @@ -1,4 +0,0 @@ -prepare(sprintf("SELECT id FROM %s WHERE - MATCH(name, body) AGAINST(? IN BOOLEAN MODE) LIMIT 20", Attribute::TABLE)); - - if($Statement->execute([$search])) { - return $Statement->fetchAll($Database::FETCH_COLUMN); - } - - return []; - } -} diff --git a/core/namespace/Page/Repository.php b/core/namespace/Page/Repository.php new file mode 100644 index 0000000..b76ef85 --- /dev/null +++ b/core/namespace/Page/Repository.php @@ -0,0 +1,17 @@ +Database->prepare($query); + $Statement->execute([$User->getID()]); + + return $Statement->fetchColumn(); + } +} diff --git a/core/namespace/Post/Attribute.php b/core/namespace/Post/Attribute.php deleted file mode 100644 index 20aafae..0000000 --- a/core/namespace/Post/Attribute.php +++ /dev/null @@ -1,22 +0,0 @@ -prepare(sprintf("SELECT id FROM %s WHERE - ({$Y} IS NULL OR YEAR(time_insert) = {$Y}) AND - ({$M} IS NULL OR MONTH(time_insert) = {$M}) AND - ({$D} IS NULL OR DAY(time_insert) = {$D}) AND - MATCH(name, body) AGAINST(? IN BOOLEAN MODE) LIMIT 20", Attribute::TABLE)); - - if($Statement->execute([$search])) { - return $Statement->fetchAll($Database::FETCH_COLUMN); - } - - return []; - } -} diff --git a/core/namespace/Post/Repository.php b/core/namespace/Post/Repository.php new file mode 100644 index 0000000..5a3d834 --- /dev/null +++ b/core/namespace/Post/Repository.php @@ -0,0 +1,17 @@ +Database->prepare($query); + $Statement->execute([$User->getID()]); + + return $Statement->fetchColumn(); + } +} diff --git a/core/namespace/Repository.php b/core/namespace/Repository.php new file mode 100644 index 0000000..2c238b8 --- /dev/null +++ b/core/namespace/Repository.php @@ -0,0 +1,323 @@ +Database = $Database; + } + + #=============================================================================== + # Adds an entity to the runtime cache + #=============================================================================== + protected function storeInstance(int $identifier, EntityInterface $Entity) { + return $this->entities[$identifier] = $Entity; + } + + #=============================================================================== + # Adds an array of entities to the runtime cache + #=============================================================================== + protected function storeMultipleInstances(array $entities) { + foreach($entities as $Entity) { + $this->storeInstance($Entity->getID(), $Entity); + } + + return $entities; + } + + #=============================================================================== + # Gets an entity from the runtime cache + #=============================================================================== + protected function fetchInstance(int $identifier) { + return $this->entities[$identifier] ?? FALSE; + } + + #=============================================================================== + # Removes an entity from the runtime cache + #=============================================================================== + protected function removeInstance(int $identifier) { + if(isset($this->cache[$identifier])) { + unset($this->cache[$identifier]); + } + } + + #=========================================================================== + # Insert entity + #=========================================================================== + public function insert(EntityInterface $Entity): bool { + $attributes = $Entity->getFilteredAttributes(); + + foreach($attributes as $field => $value) { + $fields[] = $field; + $values[] = '?'; + } + + $fields = implode(', ', $fields ?? []); + $values = implode(', ', $values ?? []); + + $query = 'INSERT INTO %s (%s) VALUES(%s)'; + $query = sprintf($query, static::getTableName(), $fields, $values); + + $Statement = $this->Database->prepare($query); + return $Statement->execute(array_values($attributes)); + } + + #=========================================================================== + # Update entity + #=========================================================================== + public function update(EntityInterface $Entity): bool { + $attributes = $Entity->getFilteredAttributes(); + + foreach($attributes as $field => $value) { + $params[] = "$field = ?"; + } + + $params = implode(', ', $params ?? []); + + $query = 'UPDATE %s SET %s WHERE id = '.intval($Entity->getID()); + $query = sprintf($query, static::getTableName(), $params); + + $Statement = $this->Database->prepare($query); + return $Statement->execute(array_values($attributes)); + } + + #=========================================================================== + # Delete entity + #=========================================================================== + public function delete(EntityInterface $Entity): bool { + $query = 'DELETE FROM %s WHERE id = ?'; + $query = sprintf($query, static::getTableName()); + + $Statement = $this->Database->prepare($query); + return $Statement->execute([$Entity->getID()]); + } + + #=========================================================================== + # Find entity based on primary key + #=========================================================================== + public function find(int $id): ?EntityInterface { + if($Entity = $this->fetchInstance($id)) { + return $Entity; + } + + return $this->findBy('id', $id); + } + + #=============================================================================== + # Find entity based on specific field comparison + #=============================================================================== + public function findBy(string $field, $value): ?EntityInterface { + $query = 'SELECT * FROM %s WHERE %s = ?'; + $query = sprintf($query, static::getTableName(), $field); + + $Statement = $this->Database->prepare($query); + $Statement->execute([$value]); + + if($Entity = $Statement->fetchObject(static::getClassName())) { + $this->storeInstance($Entity->getID(), $Entity); + return $Entity; + } + + return NULL; + } + + #=============================================================================== + # Find previous entitiy + #=============================================================================== + public function findPrev(EntityInterface $Entity): ?EntityInterface { + $query = 'SELECT * FROM %s WHERE time_insert < ? ORDER BY time_insert DESC LIMIT 1'; + $query = sprintf($query, static::getTableName()); + + $Statement = $this->Database->prepare($query); + $Statement->execute([$Entity->get('time_insert')]); + + if($Entity = $Statement->fetchObject(static::getClassName())) { + $this->storeInstance($Entity->getID(), $Entity); + return $Entity; + } + + return NULL; + } + + #=============================================================================== + # Find next entity + #=============================================================================== + public function findNext(EntityInterface $Entity): ?EntityInterface { + $query = 'SELECT * FROM %s WHERE time_insert > ? ORDER BY time_insert ASC LIMIT 1'; + $query = sprintf($query, static::getTableName()); + + $Statement = $this->Database->prepare($query); + $Statement->execute([$Entity->get('time_insert')]); + + if($Entity = $Statement->fetchObject(static::getClassName())) { + $this->storeInstance($Entity->getID(), $Entity); + return $Entity; + } + + return NULL; + } + + #=========================================================================== + # Find last (which means the newest) entity + #=========================================================================== + public function getLast(): ?EntityInterface { + $query = 'SELECT * FROM %s ORDER BY time_insert DESC LIMIT 1'; + $query = sprintf($query, static::getTableName()); + + $Statement = $this->Database->query($query); + + if($Entity = $Statement->fetchObject(static::getClassName())) { + $this->storeInstance($Entity->getID(), $Entity); + return $Entity; + } + + return NULL; + } + + #=========================================================================== + # Get entity count + #=========================================================================== + public function getCount(): int { + $query = 'SELECT COUNT(id) FROM %s'; + $query = sprintf($query, static::getTableName()); + + return $this->Database->query($query)->fetchColumn(); + } + + #=========================================================================== + # Get paginated entity list + #=========================================================================== + public function getPaginated(string $order, int $limit, int $offset = 0): array { + return $this->getAll([], $order, "$offset,$limit"); + } + + #=========================================================================== + # Get all entities + #=========================================================================== + public function getAll(array $filter = [], string $order = null, string $limit = null): array { + $select = 'SELECT * FROM '.static::getTableName(); + $wheres = []; + $params = []; + + if(!empty($filter)) { + foreach($filter as $column => $value) { + $wheres[] = "$column = ?"; + $params[] = $value; + } + + $where = 'WHERE '.implode(' AND ', $wheres); + } + + if($order) { + $order = "ORDER BY $order"; + } + + if($limit) { + $limit = "LIMIT $limit"; + } + + $query = "$select %s %s %s"; + $query = sprintf($query, $where ?? '', $order ?? '', $limit ?? ''); + + $Statement = $this->Database->prepare($query); + $Statement->execute($params); + + if($entities = $Statement->fetchAll($this->Database::FETCH_CLASS, static::getClassName())) { + $this->storeMultipleInstances($entities); + return $entities; + } + + return []; + } + + #=============================================================================== + # Get entities based on search query + #=============================================================================== + public function search(string $search, array $filter = []): array { + if($search === '*') { + return $this->getAll([], NULL, 20); + } + + if(strlen($filter['year'] ?? '') !== 0) { + $extend[] = 'YEAR(time_insert) = ? AND'; + $params[] = $filter['year']; + } + + if(strlen($filter['month'] ?? '') !== 0) { + $extend[] = 'MONTH(time_insert) = ? AND'; + $params[] = $filter['month']; + } + + if(strlen($filter['day'] ?? '') !== 0) { + $extend[] = 'DAY(time_insert) = ? AND'; + $params[] = $filter['day']; + } + + $dateparts = implode(' ', $extend ?? []); + + $query = 'SELECT * FROM %s WHERE %s MATCH(name, body) + AGAINST(? IN BOOLEAN MODE) LIMIT 20'; + $query = sprintf($query, static::getTableName(), $dateparts); + + $Statement = $this->Database->prepare($query); + $Statement->execute(array_merge($params ?? [], [$search])); + + if($entities = $Statement->fetchAll($this->Database::FETCH_CLASS, static::getClassName())) { + $this->storeMultipleInstances($entities); + return $entities; + } + + return []; + } + + #=============================================================================== + # Get a list of distinct days + #=============================================================================== + public function getDistinctDays(): array { + $query = 'SELECT DISTINCT DAY(time_insert) AS d FROM %s ORDER BY d'; + $query = sprintf($query, static::getTableName()); + + $Statement = $this->Database->query($query); + + if($result = $Statement->fetchAll($this->Database::FETCH_COLUMN)) { + return $result; + } + + return []; + } + + #=============================================================================== + # Get a list of distinct months + #=============================================================================== + public function getDistinctMonths(): array { + $query = 'SELECT DISTINCT MONTH(time_insert) AS m FROM %s ORDER BY m'; + $query = sprintf($query, static::getTableName()); + + $Statement = $this->Database->query($query); + + if($result = $Statement->fetchAll($this->Database::FETCH_COLUMN)) { + return $result; + } + + return []; + } + + #=============================================================================== + # Get a list of distinct years + #=============================================================================== + public function getDistinctYears(): array { + $query = 'SELECT DISTINCT YEAR(time_insert) AS y FROM %s ORDER BY y'; + $query = sprintf($query, static::getTableName()); + + $Statement = $this->Database->query($query); + + if($result = $Statement->fetchAll($this->Database::FETCH_COLUMN)) { + return $result; + } + + return []; + } +} diff --git a/core/namespace/User/Attribute.php b/core/namespace/User/Attribute.php deleted file mode 100644 index ccd31b5..0000000 --- a/core/namespace/User/Attribute.php +++ /dev/null @@ -1,24 +0,0 @@ -