PHP Classes

File: src/EasyStatement.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   EasyDB   ???   Download  
File: src/???
Role: Class source
Content type: text/plain
Description: Class source
Class: EasyDB
Simple Database Abstraction Layer around PDO
Author: By
Last change: Mark nullable parameters as nullable
Date: -2 hours ago
Size: 11,610 bytes
 

Contents

Class file image Download
<?php namespace ParagonIE\EasyDB; use ParagonIE\EasyDB\Exception\{ MustBeEmpty, MustBeNonEmpty }; use RuntimeException; use TypeError; use function array_merge, array_reduce, count, is_object, is_string, sprintf, str_repeat, str_replace, trim; /** * Class EasyStatement * @package ParagonIE\EasyDB */ class EasyStatement { /** * @var array<int, array{type:string, condition:self|string, values?:array<int, mixed>}> $parts */ private array $parts = []; private ?EasyStatement $parent; private bool $allowEmptyInStatements = false; public function count(): int { return count($this->parts); } /** * Open a new statement. * * @return self * @psalm-suppress UnsafeInstantiation */ public static function open(): self { return new static(); } /** * @param bool $allow * @return self */ public function setEmptyInStatementsAllowed(bool $allow = false): self { $this->allowEmptyInStatements = $allow; return $this; } /** * Alias for andWith(). * * @param EasyStatement|string $condition * @param mixed ...$values * @return self * @psalm-taint-sink sql $condition */ public function with(EasyStatement|string $condition, ...$values): self { return $this->andWith($condition, ...$values); } /** * Add a condition that will be applied with a logical "AND". * * @param string|self $condition * @param mixed ...$values * @return self * * @psalm-taint-sink sql $condition * * @throws MustBeEmpty */ public function andWith(EasyStatement|string $condition, ...$values): self { if ($condition instanceof EasyStatement) { if (!empty($values)) { throw new MustBeEmpty("EasyStatement provided; must be only argument."); } $values = $condition->values(); $condition = '(' . $condition . ')'; } return $this->andWithString($condition, ...$values); } /** * Add a condition that will be applied with a logical "AND". * * @param string $condition * @param mixed ...$values * * @return self * * @psalm-taint-sink sql $condition */ public function andWithString(string $condition, ...$values): self { $this->parts[] = [ 'type' => 'AND', 'condition' => $condition, 'values' => $values, ]; return $this; } /** * Add a condition that will be applied with a logical "OR". * * @param string|self $condition * @param mixed ...$values * @return self * * @psalm-taint-sink sql $condition */ public function orWith(EasyStatement|string $condition, ...$values): self { if ($condition instanceof EasyStatement) { if (!empty($values)) { throw new MustBeEmpty("EasyStatement provided; must be only argument."); } $values = $condition->values(); $condition = '(' . $condition . ')'; } return $this->orWithString($condition, ...$values); } /** * Add a condition that will be applied with a logical "OR". * * @param string $condition * @param mixed ...$values * * @return self * * @psalm-taint-sink sql $condition */ public function orWithString(string $condition, ...$values): self { $this->parts[] = [ 'type' => 'OR', 'condition' => $condition, 'values' => $values, ]; return $this; } /** * Alias for andIn(). * * @param string $condition * @param array $values * * @return self * @throws MustBeNonEmpty * * @psalm-taint-sink sql $condition */ public function in(string $condition, array $values): self { return $this->andIn($condition, $values); } /** * Add an IN condition that will be applied with a logical "AND". * * Instead of using ? to denote the placeholder, ?* must be used! * * @param string $condition * @param array $values * * @return self * * @throws MustBeNonEmpty * @throws RuntimeException * @throws TypeError * * @psalm-taint-sink sql $condition */ public function andIn(string $condition, array $values): self { if (count($values) < 1) { if (!$this->allowEmptyInStatements) { throw new MustBeNonEmpty(); } // Add a closed failure: $this->parts[] = [ 'type' => 'AND', 'condition' => '1 = 0', 'values' => [] ]; return $this; } try { return $this->andWith( $this->unpackCondition($condition, count($values)), ...$values ); } catch (MustBeEmpty $ex) { throw new RuntimeException("Invalid state reached", 0, $ex); } } /** * Add an IN condition that will be applied with a logical "OR". * * Instead of using "?" to denote the placeholder, "?*" must be used! * * @param string $condition * @param array $values * @return self * * @throws MustBeNonEmpty * * @psalm-taint-sink sql $condition */ public function orIn(string $condition, array $values): self { if (count($values) < 1) { if (!$this->allowEmptyInStatements) { throw new MustBeNonEmpty(); } return $this; } try { return $this->orWith( $this->unpackCondition($condition, count($values)), ...$values ); } catch (MustBeEmpty $ex) { throw new RuntimeException("Invalid state reached", 0, $ex); } } /** * Alias for andGroup(). * * @return self */ public function group(): self { return $this->andGroup(); } /** * Start a new grouping that will be applied with a logical "AND". * * Exit the group with endGroup(). * * @return self */ public function andGroup(): self { $group = new self($this); $group->setEmptyInStatementsAllowed($this->allowEmptyInStatements); $this->parts[] = [ 'type' => 'AND', 'condition' => $group, ]; return $group; } /** * Start a new grouping that will be applied with a logical "OR". * * Exit the group with endGroup(). * * @return self */ public function orGroup(): self { $group = new self($this); $group->setEmptyInStatementsAllowed($this->allowEmptyInStatements); $this->parts[] = [ 'type' => 'OR', 'condition' => $group, ]; return $group; } /** * Alias for endGroup(). * * @return self */ public function end(): self { return $this->endGroup(); } /** * Exit the current grouping and return the parent statement. * * @return self * * @throws RuntimeException * If the current statement has no parent context. */ public function endGroup(): self { if (empty($this->parent)) { throw new RuntimeException('Already at the top of the statement'); } return $this->parent; } /** * Compile the current statement into PDO-ready SQL. * * @return string */ public function sql(): string { if (empty($this->parts)) { return '1 = 1'; } return array_reduce( $this->parts, /** * @psalm-param array{type:string, condition:self|string, values?:array<int, mixed>} $part */ function (string $sql, array $part): string { /** @var string|self $condition */ $condition = $part['condition']; if ($this->isGroup($condition)) { // (...) if (is_string($condition)) { $statement = '(' . $condition . ')'; } else { $statement = '(' . $condition->sql() . ')'; } } else { // foo = ? $statement = $condition; } /** @var string $statement */ $statement = (string) $statement; $part['type'] = (string) $part['type']; if ($sql) { $statement = match ($part['type']) { 'AND', 'OR' => $part['type'] . ' ' . $statement, default => throw new RuntimeException( sprintf('Invalid joiner %s', $part['type']) ), }; } /** @psalm-taint-sink sql */ return trim($sql . ' ' . $statement); }, '' ); } /** * Get the parameters attached to this statement. * * @return array */ public function values(): array { return (array) array_reduce( $this->parts, /** * @psalm-param array{type:string, condition:self|string, values?:array<int, mixed>} $part */ function (array $values, array $part): array { if ($this->isGroup($part['condition'])) { /** @var EasyStatement $condition */ $condition = $part['condition']; return array_merge( $values, $condition->values() ); } elseif (!isset($part['values'])) { return $values; } return array_merge($values, $part['values']); }, [] ); } /** * Convert the statement to a string. * * @return string */ public function __toString(): string { return $this->sql(); } /** * Don't instantiate directly. Instead, use open() (static method). * * EasyStatement constructor. * @param EasyStatement|null $parent */ protected function __construct(?EasyStatement $parent = null) { $this->parent = $parent; } /** * Check if a condition is a sub-group. * * @param mixed $condition * * @return bool */ protected function isGroup(mixed $condition): bool { if (!is_object($condition)) { return false; } return $condition instanceof EasyStatement; } /** * Replace a grouped placeholder with a list of placeholders. * * Given a count of 3, the placeholder ?* will become ?, ?, ? * * @param string $condition * @param int $count * * @return string * * @psalm-taint-sink sql $condition */ private function unpackCondition(string $condition, int $count): string { // Replace a grouped placeholder with an matching count of placeholders. $params = '?' . str_repeat(', ?', $count - 1); return str_replace('?*', $params, $condition); } }