PHP Classes

File: src/Discretion/Struct.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   Discretion   src/Discretion/Struct.php   Download  
File: src/Discretion/Struct.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Discretion
Show contact forms and deliver encrypted mail
Author: By
Last change:
Date: 2 years ago
Size: 7,833 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace
ParagonIE\Discretion;

use
ParagonIE\ConstantTime\Base64UrlSafe;
use
ParagonIE\Discretion\Exception\{
   
DatabaseException,
   
RecordNotFound
};
use
ParagonIE\Discretion\Policies\Unique;

/**
 * Class Struct
 * @package ParagonIE\Discretion
 */
abstract class Struct
{
    const
TABLE_NAME = '';
    const
PRIMARY_KEY = '';
    const
DB_FIELD_NAMES = [];
    const
BOOLEAN_FIELDS = [];

   
/** @var int $id */
   
protected $id = 0;

   
/** @var \DateTimeImmutable|null $created */
   
protected $created = null;

   
/** @var \DateTimeImmutable|null $modified */
   
protected $modified = null;

   
/** @var array<string, Struct> $objectCache */
   
protected static $objectCache = [];

   
/** @var string $runtimeCacheKey */
   
protected static $runtimeCacheKey = '';

   
/**
     * Get a new struct by its ID
     * @param int $id
     * @return static
     *
     * @throws RecordNotFound
     * @throws \Error
     */
   
public static function byId(int $id): self
   
{
        if (empty(static::
TABLE_NAME) || empty(static::PRIMARY_KEY) || empty(static::DB_FIELD_NAMES)) {
            throw new \
Error('Struct does not define necessary constants');
        }
       
$self = new static();
        if (
$self instanceof Unique) {
            if (\
array_key_exists($self->getCacheKey($id), self::$objectCache)) {
                return
self::$objectCache[$self->getCacheKey($id)];
            }
        }
       
$db = Discretion::getDatabase();
       
/** @var array<string, mixed> $row */
       
$row = $db->row(
           
"SELECT * FROM " .
               
$db->escapeIdentifier((string) static::TABLE_NAME) .
           
" WHERE " .
               
$db->escapeIdentifier((string) static::PRIMARY_KEY) .
           
" = ?",
           
$id
       
);
        if (empty(
$row)) {
            throw new
RecordNotFound(static::class . '::' . $id);
        }
       
/** @psalm-suppress MixedAssignment */
       
foreach (static::DB_FIELD_NAMES as $field => $property) {
           
/**
             * @psalm-suppress MixedArrayOffset
             * @psalm-suppress MixedAssignment
             */
           
$self->{$property} = $row[$field];
        }
        if (isset(
$row['created'])) {
           
$self->created = new \DateTimeImmutable((string) $row['created']);
        }
        if (isset(
$row['modified'])) {
           
$self->modified = new \DateTimeImmutable((string) $row['modified']);
        }
        if (
$self instanceof Unique) {
           
self::$objectCache[$self->getCacheKey($id)] = $self;
        }
        return
$self;
    }

   
/**
     * @param int $id
     * @return string
     * @throws \Error
     * @throws \SodiumException
     * @throws \TypeError
     */
   
public function getCacheKey(int $id = 0): string
   
{
        if (empty(static::
$runtimeCacheKey)) {
            static::
$runtimeCacheKey = \random_bytes(
                \
ParagonIE_Sodium_Compat::CRYPTO_SHORTHASH_KEYBYTES
           
);
        }

       
$plaintext = \json_encode([
           
'class' => \get_class($this),
           
'id' => $id > 0 ? $id : $this->id
       
]);
        if (!\
is_string($plaintext)) {
            throw new \
Error('Could not calculate cache key');
        }
        return
Base64UrlSafe::encode(
            \
ParagonIE_Sodium_Compat::crypto_shorthash(
               
$plaintext,
                static::
$runtimeCacheKey
           
)
        );
    }

   
/**
     * @return int
     * @throws DatabaseException
     */
   
public function id(): int
   
{
        if (!
$this->id) {
            throw new
DatabaseException('Record does not have a primary key. It may have not been created yet.');
        }
        return
$this->id;
    }

   
/**
     * @return array
     */
   
public static function getRuntimeCache(): array
    {
        return static::
$objectCache;
    }

   
/**
     * @return bool
     * @throws \Exception
     */
   
public function create(): bool
   
{
        if (
$this->id) {
            return
$this->update();
        }
       
$db = Discretion::getDatabase();
       
$db->beginTransaction();

       
/** @var array<string, mixed> $fields */
       
$fields = [];
       
/** @psalm-suppress MixedAssignment */
       
foreach (static::DB_FIELD_NAMES as $field => $property) {
            if (
$field === static::PRIMARY_KEY) {
               
// No
               
continue;
            }
            if (\
in_array($field, (array) static::BOOLEAN_FIELDS, true)) {
               
/** @psalm-suppress MixedArrayOffset */
               
$fields[$field] = Discretion::getDatabaseBoolean(
                    !empty(
$this->{$property})
                );
            } else {
               
/** @psalm-suppress MixedArrayOffset */
               
$fields[$field] = $this->{$property};
            }
        }
       
$this->id = (int) $db->insertGet(
            (string) (static::
TABLE_NAME),
           
$fields,
            (string) (static::
PRIMARY_KEY)
        );
        if (
$this instanceof Unique) {
           
self::$objectCache[$this->getCacheKey()] = $this;
        }
        return
$db->commit();
    }

   
/**
     * @return bool
     * @throws \Exception
     */
   
public function update(): bool
   
{
        if (!(
$this->id)) {
            return
$this->create();
        }
       
$db = Discretion::getDatabase();
       
$db->beginTransaction();

       
/** @var array<string, mixed> $fields */
       
$fields = [];
       
/** @psalm-suppress MixedAssignment */
       
foreach (static::DB_FIELD_NAMES as $field => $property) {
            if (!\
is_string($field)) {
                throw new \
TypeError('Field name must be a string');
            }
            if (
$field === static::PRIMARY_KEY) {
               
// No
               
continue;
            }
            if (\
in_array($field, (array) static::BOOLEAN_FIELDS, true)) {
               
$fields[$field] = Discretion::getDatabaseBoolean(
                    !empty(
$this->{$property})
                );
            } else {
               
/** @psalm-suppress MixedAssignment */
               
$fields[$field] = $this->{$property};
            }
        }
       
$db->update(
            (string) (static::
TABLE_NAME),
           
$fields,
            [static::
PRIMARY_KEY => $this->id]
        );
        return
$db->commit();
    }

   
/**
     * Get the property from the object.
     *
     * @param string $name
     * @return mixed
     * @throws \InvalidArgumentException If the property does not exist.
     */
   
public function __get(string $name)
    {
        if (!\
property_exists($this, $name)) {
            throw new \
InvalidArgumentException('Property ' . $name . ' does not exist.');
        }
        return
$this->{$name};
    }

   
/**
     * Strict-typed property setter.
     *
     * @param string $name
     * @param mixed $value
     * @return void
     * @throws \TypeError
     */
   
public function __set(string $name, $value)
    {
        if (!\
property_exists($this, $name)) {
            throw new \
InvalidArgumentException('Property ' . $name . ' does not exist.');
        }

        if (
$name === 'id') {
           
// RESERVED
           
throw new \InvalidArgumentException('Cannot override an object\'s primary key.');
        }

        if (!\
is_null($this->{$name})) {
           
/* Enforce type strictness if only if property had a pre-established type. */
           
$propType = Discretion::getGenericType($this->{$name});
           
$valueType = Discretion::getGenericType($value);
            if (
$propType !== $valueType) {
                throw new \
TypeError('Property ' . $name . ' expects type ' . $propType . ', ' . $valueType . ' given.');
            }
        }
       
/** @psalm-suppress MixedAssignment */
       
$this->{$name} = $value;
    }
}