Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
JsonExtract
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 parsePathSegments
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Db\Sql;
16
17/**
18 * Json Extract expression class
19 *
20 * @category   Pop
21 * @package    Pop\Db
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    7.0.0
26 */
27class JsonExtract
28{
29
30    /**
31     * The rendered, dialect-specific extraction expression
32     * @var string
33     */
34    protected string $rendered;
35
36    /**
37     * Constructor
38     *
39     * Instantiate the JSON extract expression object. Renders immediately - this object is
40     * immutable once built, unlike Select, whose state can still change after construction.
41     *
42     * @param  AbstractSql $sql
43     * @param  string      $column
44     * @param  string      $path
45     * @throws Exception
46     */
47    public function __construct(AbstractSql $sql, string $column, string $path)
48    {
49        if ($sql->isMysql()) {
50            $this->rendered = 'JSON_UNQUOTE(JSON_EXTRACT(' . $sql->quoteId($column) . ', ' . $sql->quote($path) . '))';
51        } else if ($sql->isPgsql()) {
52            $segments = self::parsePathSegments($path);
53            if (count($segments) <= 1) {
54                $key = (count($segments) === 1) ? $segments[0] : '';
55                $this->rendered = $sql->quoteId($column) . '->>' . $sql->quote($key);
56            } else {
57                $this->rendered = $sql->quoteId($column) . '#>>' . $sql->quote('{' . implode(',', $segments) . '}');
58            }
59        } else if ($sql->isSqlite()) {
60            $this->rendered = 'json_extract(' . $sql->quoteId($column) . ', ' . $sql->quote($path) . ')';
61        } else if ($sql->isSqlsrv()) {
62            $this->rendered = 'JSON_VALUE(' . $sql->quoteId($column) . ', ' . $sql->quote($path) . ')';
63        } else {
64            throw new Exception('Error: Unsupported database type for JSON extraction.');
65        }
66    }
67
68    /**
69     * Parse a MySQL-style JSONPath string ('$.foo.bar', '$.tags[0]') into an ordered list of
70     * path segments ('foo', 'bar' / 'tags', '0'), for dialects (PostgreSQL) that address by
71     * segment array rather than JSONPath syntax directly
72     *
73     * @param  string $path
74     * @return array
75     */
76    public static function parsePathSegments(string $path): array
77    {
78        $path = ltrim($path, '$');
79        $path = preg_replace('/\[(\d+)\]/', '.$1', $path);
80        $path = trim($path, '.');
81
82        return ($path === '') ? [] : explode('.', $path);
83    }
84
85    /**
86     * Render the expression string
87     *
88     * @return string
89     */
90    public function __toString(): string
91    {
92        return $this->rendered;
93    }
94
95}