PHP Classes

File: src/Discretion/Struct.php

Recommend this page to a friend!
  Packages 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: 5 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;
    }
}