$className The name of the class onto which the JSON will be mapped * @return Type The domain type */ public static function mapDocFromJson(string $columnName, array $result, string $className): mixed { if (is_null(self::$mapper)) { self::$mapper = new \JsonMapper(); } $mapped = new $className(); self::$mapper->map(json_decode($result[$columnName]), $mapped); return $mapped; } /** * Map a domain type from the JSON document retrieved * * @param array $result An associative array with a single result to be mapped * @param class-string $className The name of the class onto which the JSON will be mapped * @return Type The domain type */ public static function mapFromJson(array $result, string $className): mixed { return self::mapDocFromJson('data', $result, $className); } /** * Execute a document-focused statement that does not return results * * @param string $query The query to be executed * @param string $docId The ID of the document on which action should be taken * @param array|object $document The array or object representing the document */ private static function executeNonQuery(string $query, string $docId, array|object $document) { $nonQuery = pdo()->prepare($query); $nonQuery->bindParam('@id', $docId); $nonQuery->bindParam('@data', Query::jsonbDocParam($document)); $nonQuery->execute(); } /** * Insert a document * * @param string $tableName The name of the table into which a document should be inserted * @param string $docId The ID of the document to be inserted * @param array|object $document The array or object representing the document */ public static function insert(string $tableName, string $docId, array|object $document) { self::executeNonQuery(Query::insert($tableName), $docId, $document); } /** * Save (upsert) a document * * @param string $tableName The name of the table into which a document should be inserted * @param string $docId The ID of the document to be inserted * @param array|object $document The array or object representing the document */ public static function save(string $tableName, string $docId, array|object $document) { self::executeNonQuery(Query::save($tableName), $docId, $document); } /** * Count all documents in a table * * @param string $tableName The name of the table in which documents should be counted * @return int The number of documents in the table */ public static function countAll(string $tableName): int { $result = pdo()->query(Query::countAll($tableName))->fetch(\PDO::FETCH_ASSOC); return intval($result['it']); } /** * Count documents in a table by JSON containment `@>` * * @param string $tableName The name of the table in which documents should be counted * @param array|object $criteria The criteria for the JSON containment query * @return int The number of documents in the table matching the JSON containment query */ public static function countByContains(string $tableName, array|object $criteria): int { $query = pdo()->prepare(Query::countByContains($tableName)); $query->bindParam('@criteria', Query::jsonbDocParam($criteria)); $query->execute(); $result = $query->fetch(\PDO::FETCH_ASSOC); return intval($result['it']); } /** * Count documents in a table by JSON Path match `@?` * * @param string $tableName The name of the table in which documents should be counted * @param string $jsonPath The JSON Path to be matched * @return int The number of documents in the table matching the JSON Path */ public static function countByJsonPath(string $tableName, string $jsonPath): int { $query = pdo()->prepare(Query::countByContains($tableName)); $query->bindParam('@path', $jsonPath); $query->execute(); $result = $query->fetch(\PDO::FETCH_ASSOC); return intval($result['it']); } /** * Determine if a document exists for the given ID * * @param string $tableName The name of the table in which existence should be checked * @param string $docId The ID of the document whose existence should be checked * @return bool True if the document exists, false if not */ public static function existsById(string $tableName, string $docId): bool { $query = pdo()->prepare(Query::existsById($tableName)); $query->bindParam('@id', $docId); $query->execute(); $result = $query->fetch(\PDO::FETCH_ASSOC); return boolval($result['it']); } /** * Determine if documents exist by JSON containment `@>` * * @param string $tableName The name of the table in which existence should be checked * @param array|object $criteria The criteria for the JSON containment query * @return int True if any documents in the table match the JSON containment query, false if not */ public static function existsByContains(string $tableName, array|object $criteria): bool { $query = pdo()->prepare(Query::existsByContains($tableName)); $query->bindParam('@criteria', Query::jsonbDocParam($criteria)); $query->execute(); $result = $query->fetch(\PDO::FETCH_ASSOC); return boolval($result['it']); } /** * Determine if documents exist by JSON Path match `@?` * * @param string $tableName The name of the table in which existence should be checked * @param string $jsonPath The JSON Path to be matched * @return int True if any documents in the table match the JSON Path, false if not */ public static function existsByJsonPath(string $tableName, string $jsonPath): bool { $query = pdo()->prepare(Query::existsByJsonPath($tableName)); $query->bindParam('@path', $jsonPath); $query->execute(); $result = $query->fetch(\PDO::FETCH_ASSOC); return boolval($result['it']); } /** * Map the results of a query to domain type objects * * @param \PDOStatement $stmt The statement with the query to be run * @param class-string $className The type of document to be mapped * @return array The documents matching the query */ private static function mapResults(\PDOStatement $stmt, string $className): array { return array_map(fn ($it) => self::mapFromJson($it, $className), $stmt->fetchAll(\PDO::FETCH_ASSOC)); } /** * Retrieve all documents in a table * * @param string $tableName The table from which all documents should be retrieved * @param class-string $className The type of document to be retrieved * @return array An array of documents */ public static function findAll(string $tableName, string $className): array { return self::mapResults(pdo()->query(Query::selectFromTable($tableName)), $className); } /** * Retrieve a document by its ID * * @param string $tableName The table from which a document should be retrieved * @param string $docId The ID of the document to retrieve * @param class-string $className The type of document to retrieve * @return Type|null The document, or null if it is not found */ public static function findById(string $tableName, string $docId, string $className): mixed { $query = pdo()->prepare(Query::findById($tableName)); $query->bindParam(':id', $docId); $query->execute(); $result = $query->fetch(\PDO::FETCH_ASSOC); return $result ? self::mapFromJson($result, $className) : null; } /** * Create a JSON containment query * * @param string $tableName The table from which documents should be retrieved * @param array|object $criteria The criteria for the JSON containment query * @return \PDOStatement An executed query ready to be fetched */ private static function queryByContains(string $tableName, array|object $criteria): \PDOStatement { $query = pdo()->prepare(Query::findByContains($tableName)); $query->bindParam('@criteria', Query::jsonbDocParam($criteria)); $query->execute(); return $query; } /** * Retrieve documents in a table via JSON containment `@>` * * @param string $tableName The table from which documents should be retrieved * @param array|object $criteria The criteria for the JSON containment query * @param class-string $className The type of document to be retrieved * @return array Documents matching the JSON containment query */ public static function findByContains(string $tableName, array|object $criteria, string $className): array { return self::mapResults(self::queryByContains($tableName, $criteria), $className); } /** * Retrieve the first matching document via JSON containment `@>` * * @param string $tableName The table from which documents should be retrieved * @param array|object $criteria The criteria for the JSON containment query * @param class-string $className The type of document to be retrieved * @return Type|null The document, or null if none match */ public static function findFirstByContains(string $tableName, array|object $criteria, string $className): mixed { $query = self::queryByContains($tableName, $criteria); $result = $query->fetch(\PDO::FETCH_ASSOC); return $result ? self::mapFromJson($result, $className) : null; } /** * Retrieve documents in a table via JSON Path match `@?` * * @param string $tableName The table from which documents should be retrieved * @param string $jsonPath The JSON Path to be matched * @return \PDOStatement An executed query ready to be fetched */ private static function queryByJsonPath(string $tableName, string $jsonPath): \PDOStatement { $query = pdo()->prepare(Query::findByJsonPath($tableName)); $query->bindParam('@path', $jsonPath); $query->execute(); return $query; } /** * Retrieve documents in a table via JSON Path match `@?` * * @param string $tableName The table from which documents should be retrieved * @param string $jsonPath The JSON Path to be matched * @param class-string $className The type of document to be retrieved * @return array Documents matching the JSON Path */ public static function findByJsonPath(string $tableName, string $jsonPath, string $className): array { return self::mapResults(self::queryByJsonPath($tableName, $jsonPath), $className); } /** * Retrieve the first matching document via JSON Path match `@?` * * @param string $tableName The table from which documents should be retrieved * @param string $jsonPath The JSON Path to be matched * @param class-string $className The type of document to be retrieved * @return Type|null The document, or null if none match */ public static function findFirstByJsonPath(string $tableName, string $jsonPath, string $className): mixed { $query = self::queryByJsonPath($tableName, $jsonPath); $result = $query->fetch(\PDO::FETCH_ASSOC); return $result ? self::mapFromJson($result, $className) : null; } /** * Update a full document * * @param string $tableName The table in which the document should be updated * @param string $docId The ID of the document to be updated * @param array|object $document The document to be updated */ public static function updateFull(string $tableName, string $docId, array|object $document) { self::executeNonQuery(Query::updateFull($tableName), $docId, $document); } /** * Update a partial document by its ID * * @param string $tableName The table in which the document should be updated * @param string $docId The ID of the document to be updated * @param array|object $document The partial document to be updated */ public static function updatePartialById(string $tableName, string $docId, array|object $document) { self::executeNonQuery(Query::updatePartialById($tableName), $docId, $document); } /** * Update partial documents by JSON containment `@>` * * @param string $tableName The table in which documents should be updated * @param array|object $criteria The JSON containment criteria * @param array|object $document The document to be updated */ public static function updatePartialByContains(string $tableName, array|object $criteria, array|object $document) { $query = pdo()->prepare(Query::updatePartialByContains($tableName)); $query->bindParam('@data', Query::jsonbDocParam($document)); $query->bindParam('@criteria', Query::jsonbDocParam($criteria)); $query->execute(); } /** * Update partial documents by JSON Path match `@?` * * @param string $tableName The table in which documents should be updated * @param string $jsonPath The JSON Path to be matched * @param array|object $document The document to be updated */ public static function updatePartialByJsonPath(string $tableName, string $jsonPath, array|object $document) { $query = pdo()->prepare(Query::updatePartialByContains($tableName)); $query->bindParam('@data', Query::jsonbDocParam($document)); $query->bindParam('@path', $jsonPath); $query->execute(); } /** * Delete a document by its ID * * @param string $tableName The table from which a document should be deleted * @param string $docId The ID of the document to be deleted */ public static function deleteById(string $tableName, string $docId) { self::executeNonQuery(Query::deleteById($tableName), $docId, []); } /** * Delete documents by JSON containment `@>` * * @param string $tableName The table from which documents should be deleted * @param array|object $criteria The criteria for the JSON containment query */ public static function deleteByContains(string $tableName, array|object $criteria) { $query = pdo()->prepare(Query::deleteByContains($tableName)); $query->bindParam('@criteria', Query::jsonbDocParam($criteria)); $query->execute(); } /** * Delete documents by JSON Path match `@?` * * @param string $tableName The table from which documents should be deleted * @param string $jsonPath The JSON Path expression to be matched */ public static function deleteByJsonPath(string $tableName, string $jsonPath) { $query = pdo()->prepare(Query::deleteByJsonPath($tableName)); $query->bindParam('@path', $jsonPath); $query->execute(); } // TODO: custom /** * Create and execute a custom query * * @param string $sql The SQL query to execute * @param array $params An associative array of parameters for the SQL query * @return PDOStatement The query, executed and ready to be fetched */ private static function createCustomQuery(string $sql, array $params): PDOStatement { $query = pdo()->prepare($sql); foreach ($params as $name => $value) { $query->bindParam($name, $value); } $query->execute(); return $query; } /** * Retrieve documents via a custom query and mapping * * @param string $sql The SQL query to execute * @param array $params An associative array of parameters for the SQL query * @param callable $mapFunc A function that expects an associative array and returns a value of the desired type * @param class-string $className The type of document to be mapped * @return array The documents matching the query */ public static function customList(string $sql, array $params, string $className, callable $mapFunc): array { return array_map( fn ($it) => $mapFunc($it, $className), self::createCustomQuery($sql, $params)->fetchAll(\PDO::FETCH_ASSOC)); } /** * Retrieve a document via a custom query and mapping * * @param string $sql The SQL query to execute * @param array $params An associative array of parameters for the SQL query * @param callable $mapFunc A function that expects an associative array and returns a value of the desired type * @param class-string $className The type of document to be mapped * @return ?Type The document matching the query, or null if none is found */ public static function customSingle(string $sql, array $params, string $className, callable $mapFunc): mixed { $result = self::createCustomQuery($sql, $params)->fetch(\PDO::FETCH_ASSOC); return $result ? $mapFunc($result, $className) : null; } /** * Execute a custom query that does not return a result * * @param string $sql The SQL query to execute * @param array $params An associative array of parameters for the SQL query */ public static function customNonQuery(string $sql, array $params) { self::createCustomQuery($sql, $params); } }