Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
37 / 37
Entity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
18
100.00% covered (success)
100.00%
37 / 37
 __call
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
6 / 6
 create
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
13 / 13
 createOrUpdate
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 getWithPK
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
11 / 11
 jsonSerialize
n/a
0 / 0
1
n/a
0 / 0
1<?php
2
3// {{{ License
4
5// This file is part of GNU social - https://www.gnu.org/software/social
6//
7// GNU social is free software: you can redistribute it and/or modify
8// it under the terms of the GNU Affero General Public License as published by
9// the Free Software Foundation, either version 3 of the License, or
10// (at your option) any later version.
11//
12// GNU social is distributed in the hope that it will be useful,
13// but WITHOUT ANY WARRANTY; without even the implied warranty of
14// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15// GNU Affero General Public License for more details.
16//
17// You should have received a copy of the GNU Affero General Public License
18// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
19
20// }}}
21
22namespace App\Core;
23
24use App\Core\DB\DB;
25use App\Util\Exception\NotFoundException;
26use App\Util\Exception\ServerException;
27use App\Util\Formatting;
28use DateTime;
29
30/**
31 * Base class to all entities, with some utilities
32 */
33abstract class Entity implements \JsonSerializable
34{
35    public function __call(string $name , array $arguments): mixed
36    {
37        if (Formatting::startsWith($name, 'has')) {
38            $prop = Formatting::camelCaseToSnakeCase(Formatting::removePrefix($name, 'has'));
39            // https://wiki.php.net/rfc/closure_apply#proposal
40            $private_property_accessor = function ($prop) { return isset($this->{$prop}); };
41            $private_property_accessor = $private_property_accessor->bindTo($this, get_called_class());
42            return $private_property_accessor($prop);
43        }
44        throw new \BadMethodCallException('Non existent method ' . get_called_class() . "::{$name} called with arguments: " . print_r($arguments, true));
45    }
46
47    /**
48     * Create an instance of the called class or fill in the
49     * properties of $obj with the associative array $args. Doesn't
50     * persist the result
51     *
52     * @param null|mixed $obj
53     */
54    public static function create(array $args, $obj = null)
55    {
56        $class = get_called_class();
57        $obj   = $obj ?: new $class();
58        $date  = new DateTime();
59        foreach (['created', 'modified'] as $prop) {
60            if (property_exists($class, $prop)) {
61                $args[$prop] = $date;
62            }
63        }
64
65        foreach ($args as $prop => $val) {
66            if (property_exists($obj, $prop)) {
67                $set = 'set' . ucfirst(Formatting::snakeCaseToCamelCase($prop));
68                $obj->{$set}($val);
69            } else {
70                Log::error($m = "Property {$class}::{$prop} doesn't exist");
71                throw new \InvalidArgumentException($m);
72            }
73        }
74        return $obj;
75    }
76
77    /**
78     * Create a new instance, but check for duplicates
79     *
80     * @return [$obj, $is_update]
81     */
82    public static function createOrUpdate(array $args, array $find_by_keys = [])
83    {
84        $table   = DB::getTableForClass(get_called_class());
85        $find_by = $find_by_keys == [] ? $args : array_intersect_key($args, array_flip($find_by_keys));
86        try {
87            $obj = DB::findOneBy($table, $find_by);
88        } catch (NotFoundException) {
89            $obj = null;
90            // @codeCoverageIgnoreStart
91        } catch (\Exception $e) {
92            Log::unexpected_exception($e);
93            // @codeCoverageIgnoreEnd
94        }
95        $is_update = $obj !== null;
96        return [self::create($args, $obj), $is_update];
97    }
98
99    /**
100     * Get an Entity from its primary key
101     *
102     * Support multiple formats:
103     *  - mixed $values - convert to array and check next
104     *  - array[int => mixed] $values - get keys for entity and set them in order and proceed to next case
105     *  - array[string => mixed] $values - Perform a regular find
106     *
107     * Examples:
108     *     Entity::getWithPK(42);
109     *     Entity::getWithPK([42, 'foo']);
110     *     Entity::getWithPK(['key1' => 42, 'key2' => 'foo'])
111     *
112     * @return null|static
113     */
114    public static function getWithPK(mixed $values): ?self
115    {
116        $values  = is_array($values) ? $values : [$values];
117        $class   = get_called_class();
118        $keys    = DB::getPKForClass($class);
119        $find_by = [];
120        foreach ($values as $k => $v) {
121            if (is_string($k)) {
122                $find_by[$k] = $v;
123            } else {
124                $find_by[$keys[$k]] = $v;
125            }
126        }
127        try {
128            return DB::findOneBy($class, $find_by);
129        } catch (NotFoundException $e) {
130            return null;
131        }
132    }
133
134    /**
135     * Called when json_encode encounters this object. Not all
136     * entities will need json encoding, so it doesn't make sense to
137     * make this abstract
138     *
139     * @throw ServerException
140     * @codeCoverageIgnore
141     */
142    public function jsonSerialize()
143    {
144        throw new ServerException(_m('Unimplemented method'));
145    }
146}