├── LICENSE ├── composer.json └── src ├── Concerns └── ForwardsCalls.php ├── Connection.php ├── Contracts └── Pluggable.php ├── DB.php ├── Exceptions └── QueryException.php ├── Query ├── Builder.php ├── Grammar.php ├── Join.php └── ReturnType.php └── Relations ├── Relation.php ├── WithMany.php ├── WithOne.php └── WithOneOrMany.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mehedi Hasan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mehedimi/wp-query-builder", 3 | "description": "A database query builder for WordPress", 4 | "license": "MIT", 5 | "autoload": { 6 | "psr-4": { 7 | "Mehedi\\WPQueryBuilder\\": "src/" 8 | } 9 | }, 10 | "autoload-dev": { 11 | "psr-4": { 12 | "Mehedi\\WPQueryBuilderTests\\": "tests/" 13 | } 14 | }, 15 | "authors": [ 16 | { 17 | "name": "Mehedi Hasan", 18 | "email": "mehedihasansabbirmi@gmail.com", 19 | "homepage": "https://mehedi.im", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require-dev": { 24 | "phpunit/phpunit": "^9.5", 25 | "symfony/var-dumper": "^5.4", 26 | "mockery/mockery": "^1.5", 27 | "vlucas/phpdotenv": "^5.4", 28 | "fakerphp/faker": "^1.20", 29 | "phpstan/phpstan": "^1.9", 30 | "phpstan/phpstan-mockery": "^1.1", 31 | "laravel/pint": "^1.15" 32 | }, 33 | "require": { 34 | "ext-mysqli": "*", 35 | "php": ">=5.6" 36 | }, 37 | "scripts": { 38 | "test": "phpunit --testdox", 39 | "test:units": "phpunit --filter=Units --testdox", 40 | "test:features": "RUN_FEATURE_TEST=on phpunit --filter=Features --testdox", 41 | "check": "vendor/bin/phpstan analyse -c phpstan.neon", 42 | "pre-commit": "composer check && composer test", 43 | "fmt": "./vendor/bin/pint" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Concerns/ForwardsCalls.php: -------------------------------------------------------------------------------- 1 | $parameters 15 | * @return mixed 16 | * 17 | * @throws BadMethodCallException 18 | */ 19 | protected static function forwardCallTo($object, $method, $parameters) 20 | { 21 | try { 22 | return $object->{$method}(...$parameters); 23 | } catch (BadMethodCallException $e) { 24 | $pattern = '~^Call to undefined method (?P[^:]+)::(?P[^\(]+)\(\)$~'; 25 | 26 | if (! preg_match($pattern, $e->getMessage(), $matches)) { 27 | throw $e; 28 | } 29 | 30 | if ($matches['class'] != get_class($object) || 31 | $matches['method'] != $method) { 32 | throw $e; 33 | } 34 | 35 | static::throwBadMethodCallException($method); 36 | } 37 | } 38 | 39 | /** 40 | * Throw a bad method call exception for the given method. 41 | * 42 | * @param string $method 43 | * @return void 44 | * 45 | * @throws BadMethodCallException 46 | */ 47 | protected static function throwBadMethodCallException($method) 48 | { 49 | throw new BadMethodCallException(sprintf( 50 | 'Call to undefined method %s::%s()', 51 | static::class, 52 | $method 53 | )); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | |float|int|string>>|null 36 | */ 37 | protected $queryLogs; 38 | 39 | /** 40 | * Indicates whether queries are being logged. 41 | * 42 | * @var bool 43 | */ 44 | protected $loggingQueries = false; 45 | 46 | /** 47 | * Create a new connection instance using mysqli connection from $wpdb 48 | */ 49 | public function __construct(mysqli $mysqli) 50 | { 51 | $this->mysqli = $mysqli; 52 | } 53 | 54 | /** 55 | * Run a select statement against the database. 56 | * 57 | * @param string $query 58 | * @param array $bindings 59 | * @return array 60 | */ 61 | public function select($query, $bindings = []) 62 | { 63 | return $this->run($query, $bindings, function ($query, $bindings) { 64 | // For select statements, we'll simply execute the query and return an array 65 | // of the database result set. Each element in the array will be a single 66 | // row from the database table, and will either be an array or objects. 67 | try { 68 | $statement = $this->mysqli->prepare($query); 69 | } catch (mysqli_sql_exception $e) { 70 | throw new QueryException($e->getMessage(), $e->getCode()); 71 | } 72 | 73 | if ($statement === false) { 74 | throw new QueryException($this->mysqli->error); 75 | } 76 | 77 | $this->bindValues($statement, $bindings); 78 | 79 | $statement->execute(); 80 | 81 | $result = $statement->get_result(); 82 | 83 | if ($result === false) { 84 | throw new QueryException($statement->error); 85 | } 86 | 87 | return $this->getRowsFromResult($result); 88 | }); 89 | } 90 | 91 | /** 92 | * Run a SQL statement and log its execution context. 93 | * 94 | * @param string $query 95 | * @param array $bindings 96 | * @return mixed 97 | */ 98 | protected function run($query, $bindings, Closure $callback) 99 | { 100 | $start = microtime(true); 101 | 102 | $result = call_user_func($callback, $query, $bindings); 103 | 104 | $this->logQuery($query, $bindings, $this->getElapsedTime($start)); 105 | 106 | return $result; 107 | } 108 | 109 | /** 110 | * Log a query in the connection's query log. 111 | * 112 | * @param string $query 113 | * @param array $bindings 114 | * @param float $time 115 | * @return void 116 | */ 117 | protected function logQuery($query, $bindings, $time) 118 | { 119 | if ($this->loggingQueries) { 120 | $this->queryLogs[] = compact('query', 'bindings', 'time'); 121 | } 122 | } 123 | 124 | /** 125 | * Get the elapsed time since a given starting point. 126 | * 127 | * @param float $start 128 | * @return float 129 | */ 130 | protected function getElapsedTime($start) 131 | { 132 | return round((microtime(true) - $start) * 1000, 2); 133 | } 134 | 135 | /** 136 | * Bind values to their parameters in the given statement. 137 | * 138 | * @param mysqli_stmt $statement 139 | * @param array $bindings 140 | * @return void 141 | */ 142 | protected function bindValues($statement, $bindings) 143 | { 144 | if (empty($bindings)) { 145 | return; 146 | } 147 | 148 | $types = array_reduce($bindings, function ($carry, $value) { 149 | if (is_int($value)) { 150 | return $carry.'i'; 151 | } 152 | if (is_float($value)) { 153 | return $carry.'d'; 154 | } 155 | 156 | return $carry.'s'; 157 | }, ''); 158 | 159 | $statement->bind_param($types, ...$bindings); 160 | } 161 | 162 | /** 163 | * Get rows from mysqli_result 164 | * 165 | * @return array 166 | */ 167 | protected function getRowsFromResult(mysqli_result $result) 168 | { 169 | return array_map(function ($row) { 170 | return (object) $row; 171 | }, $result->fetch_all(MYSQLI_ASSOC)); 172 | } 173 | 174 | /** 175 | * Run an insert statement against the database. 176 | * 177 | * @param string $query 178 | * @param array $bindings 179 | * @return int|string 180 | */ 181 | public function insert($query, $bindings = []) 182 | { 183 | return $this->affectingStatement($query, $bindings); 184 | } 185 | 186 | /** 187 | * Execute an SQL statement and return the boolean result. 188 | * 189 | * @param string $query 190 | * @param array $bindings 191 | * @return bool 192 | */ 193 | public function statement($query, $bindings = []) 194 | { 195 | return $this->run($query, $bindings, function ($query, $bindings) { 196 | try { 197 | $statement = $this->mysqli->prepare($query); 198 | } catch (mysqli_sql_exception $e) { 199 | throw new QueryException($e->getMessage()); 200 | } 201 | 202 | if ($statement === false) { 203 | throw new QueryException($this->mysqli->error); 204 | } 205 | 206 | $this->bindValues($statement, $bindings); 207 | 208 | return $statement->execute(); 209 | }); 210 | } 211 | 212 | /** 213 | * Run update query with affected rows 214 | * 215 | * @param string $query 216 | * @param array $bindings 217 | * @return int|string 218 | */ 219 | public function update($query, $bindings = []) 220 | { 221 | return $this->affectingStatement($query, $bindings); 222 | } 223 | 224 | /** 225 | * Run an SQL statement and get the number of rows affected. 226 | * 227 | * @param string $query 228 | * @param array $bindings 229 | * @param int $returnType 230 | * @return int|string 231 | */ 232 | public function affectingStatement($query, $bindings = [], $returnType = ReturnType::AFFECTED_ROW) 233 | { 234 | return $this->run($query, $bindings, function ($query, $bindings) use ($returnType) { 235 | // For update or delete statements, we want to get the number of rows affected 236 | // by the statement and return that back to the developer. We'll first need 237 | // to execute the statement, and then we'll use affected_rows property of mysqli_stmt. 238 | try { 239 | $statement = $this->mysqli->prepare($query); 240 | } catch (mysqli_sql_exception $e) { 241 | throw new QueryException($e->getMessage()); 242 | } 243 | 244 | if ($statement === false) { 245 | throw new QueryException($this->mysqli->error); 246 | } 247 | 248 | $this->bindValues($statement, $bindings); 249 | 250 | $bool = $statement->execute(); 251 | 252 | if ($returnType === ReturnType::AFFECTED_ROW) { 253 | return $statement->affected_rows; 254 | } 255 | 256 | if ($returnType === ReturnType::INSERT_ID) { 257 | return $statement->insert_id; 258 | } 259 | 260 | return $bool; 261 | }); 262 | } 263 | 264 | /** 265 | * Run delete query with affected rows 266 | * 267 | * @param string $query 268 | * @param array $bindings 269 | * @return int|string 270 | */ 271 | public function delete($query, $bindings = []) 272 | { 273 | return $this->affectingStatement($query, $bindings); 274 | } 275 | 276 | /** 277 | * Enable the query log on the connection. 278 | * 279 | * @return void 280 | */ 281 | public function enableQueryLog() 282 | { 283 | $this->loggingQueries = true; 284 | } 285 | 286 | /** 287 | * Disable the query log on the connection. 288 | * 289 | * @return void 290 | */ 291 | public function disableQueryLog() 292 | { 293 | $this->loggingQueries = false; 294 | } 295 | 296 | /** 297 | * Get the connection query logs. 298 | * 299 | * @return array|float|int|string>>|null 300 | */ 301 | public function getQueryLog() 302 | { 303 | return $this->queryLogs; 304 | } 305 | 306 | /** 307 | * Execute a callback within a transaction 308 | * 309 | * @param int $flags 310 | * @param string|null $name 311 | * @return false|mixed 312 | */ 313 | public function transaction(callable $callback, $flags = 0, $name = null) 314 | { 315 | try { 316 | $result = call_user_func($callback); 317 | $this->commit($flags, $name); 318 | 319 | return $result; 320 | } catch (Exception $e) { 321 | $this->rollback($flags, $name); 322 | 323 | return false; 324 | } 325 | } 326 | 327 | /** 328 | * @param string $name 329 | * @param array $arguments 330 | * @return bool 331 | */ 332 | public function __call($name, $arguments) 333 | { 334 | if (! preg_match('/^(beginTransaction|commit|rollback)$/', $name)) { 335 | self::throwBadMethodCallException($name); 336 | } 337 | 338 | // @phpstan-ignore-next-line 339 | $name = strtolower(preg_replace('/(?mysqli, $name], $arguments); 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/Contracts/Pluggable.php: -------------------------------------------------------------------------------- 1 | from($table); 47 | } 48 | 49 | /** 50 | * Get the database connection from `$wpdb` 51 | * 52 | * @return Connection 53 | */ 54 | public static function getConnection() 55 | { 56 | if (is_null(self::$connection)) { 57 | global $wpdb; 58 | self::$connection = new Connection($wpdb->__get('dbh')); 59 | Grammar::getInstance()->setTablePrefix($wpdb->prefix); 60 | } 61 | 62 | return self::$connection; 63 | } 64 | 65 | /** 66 | * Apply a mixin to builder class 67 | * 68 | * @return Builder 69 | */ 70 | public static function plugin(Pluggable $plugin) 71 | { 72 | return (new Builder(self::getConnection()))->plugin($plugin); 73 | } 74 | 75 | /** 76 | * Handle dynamic method calling 77 | * 78 | * @param string $name 79 | * @param array $arguments 80 | * @return Builder 81 | */ 82 | public static function __callStatic($name, $arguments) 83 | { 84 | return self::forwardCallTo(self::getConnection(), $name, $arguments); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Exceptions/QueryException.php: -------------------------------------------------------------------------------- 1 | ', '<=', '>=', '<>', '!=', '<=>', 25 | 'like', 'like binary', 'not like', 'ilike', 26 | '&', '|', '^', '<<', '>>', '&~', 'is', 'is not', 27 | 'rlike', 'not rlike', 'regexp', 'not regexp', 28 | '~', '~*', '!~', '!~*', 'similar to', 29 | 'not similar to', 'not ilike', '~~*', '!~~*', 30 | ]; 31 | 32 | /** 33 | * This contains aggregate column and function 34 | * 35 | * @var null|array 36 | */ 37 | public $aggregate; 38 | 39 | /** 40 | * Indicate distinct query 41 | * 42 | * @var null|bool|string 43 | */ 44 | public $distinct; 45 | 46 | /** 47 | * Query table name without prefix 48 | * 49 | * @var string 50 | */ 51 | public $from; 52 | 53 | /** 54 | * Selected columns of a table 55 | * 56 | * @var string|array 57 | */ 58 | public $columns = '*'; 59 | 60 | /** 61 | * The maximum number of records to return. 62 | * 63 | * @var int | null 64 | */ 65 | public $limit; 66 | 67 | /** 68 | * The number of records to skip. 69 | * 70 | * @var int | null 71 | */ 72 | public $offset; 73 | 74 | /** 75 | * The where constraints for the query. 76 | * 77 | * @var array 78 | */ 79 | public $wheres = []; 80 | 81 | /** 82 | * The orderings for the query. 83 | * 84 | * @var array 85 | */ 86 | public $orders; 87 | 88 | /** 89 | * The table joins for the query. 90 | * 91 | * @var array|null 92 | */ 93 | public $joins; 94 | 95 | /** 96 | * The current query value bindings. 97 | * 98 | * @var array 99 | */ 100 | public $bindings = [ 101 | 'join' => [], 102 | 'where' => [], 103 | ]; 104 | 105 | /** 106 | * The groupings for the query. 107 | * 108 | * @var array|null 109 | */ 110 | public $groups; 111 | 112 | /** 113 | * With queries 114 | * 115 | * @var array|null 116 | */ 117 | public $with; 118 | 119 | /** 120 | * Query grammar instance 121 | * 122 | * @var Grammar 123 | */ 124 | public $grammar; 125 | 126 | /** 127 | * Connection instance 128 | * 129 | * @var Connection 130 | */ 131 | public $connection; 132 | 133 | /** 134 | * Create a new query builder instance. 135 | */ 136 | public function __construct(Connection $connection, Grammar $grammar = null) 137 | { 138 | $this->connection = $connection; 139 | $this->grammar = $grammar ?: Grammar::getInstance(); 140 | } 141 | 142 | /** 143 | * Set the table which the query is targeting. 144 | * 145 | * @param string $table 146 | * @return $this 147 | */ 148 | public function from($table) 149 | { 150 | $this->from = $table; 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * Use distinct query 157 | * 158 | * @param bool $column 159 | * @return $this 160 | */ 161 | public function distinct($column = true) 162 | { 163 | $this->distinct = $column; 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * Retrieve the sum of the values of a given column. 170 | * 171 | * @param string $column 172 | * @return float|int 173 | */ 174 | public function sum($column) 175 | { 176 | return $this->aggregate(__FUNCTION__, $column); 177 | } 178 | 179 | /** 180 | * Execute an aggregate function on the database. 181 | * 182 | * @param string $function 183 | * @param string $column 184 | * @return float|int 185 | */ 186 | public function aggregate($function, $column) 187 | { 188 | $this->aggregate = [$function, $column]; 189 | 190 | $data = $this->get(); 191 | 192 | if (empty($data) || ! isset($data[0]->aggregate)) { 193 | return 0; 194 | } 195 | 196 | if (is_string($data[0]->aggregate)) { 197 | return strpos($data[0]->aggregate, '.') ? floatval($data[0]->aggregate) : intval($data[0]->aggregate); 198 | } 199 | 200 | return $data[0]->aggregate; 201 | } 202 | 203 | /** 204 | * Execute the query as a "select" statement. 205 | * 206 | * @return array 207 | */ 208 | public function get() 209 | { 210 | $bindings = $this->getBindings(); 211 | 212 | $results = $this->connection->select($this->toSQL(), $bindings); 213 | 214 | if (! empty($this->with)) { 215 | foreach ($this->with as $relation) { 216 | /** @var Relation $relation */ 217 | $results = $relation->setItems($results)->load(); 218 | } 219 | } 220 | 221 | return $results; 222 | } 223 | 224 | /** 225 | * Get the current query value bindings in a flattened array. 226 | * 227 | * @return array 228 | */ 229 | public function getBindings() 230 | { 231 | return array_reduce($this->bindings, function ($bindings, $binding) { 232 | return array_merge($bindings, array_values($binding)); 233 | }, []); 234 | } 235 | 236 | /** 237 | * Select table columns 238 | * 239 | * @param string|array $columns 240 | * @return $this 241 | */ 242 | public function select($columns) 243 | { 244 | $this->columns = is_array($columns) ? $columns : func_get_args(); 245 | 246 | return $this; 247 | } 248 | 249 | /** 250 | * Returns generated SQL query 251 | * 252 | * @return string 253 | */ 254 | public function toSQL() 255 | { 256 | return $this->grammar->compileSelectComponents($this); 257 | } 258 | 259 | /** 260 | * Retrieve the "count" result of the query. 261 | * 262 | * @param string $columns 263 | * @return int 264 | */ 265 | public function count($columns = '*') 266 | { 267 | return (int) $this->aggregate(__FUNCTION__, $columns); 268 | } 269 | 270 | /** 271 | * Retrieve the average of the values of a given column. 272 | * 273 | * @param string $column 274 | * @return float|int 275 | */ 276 | public function avg($column) 277 | { 278 | return $this->aggregate(__FUNCTION__, $column); 279 | } 280 | 281 | /** 282 | * Retrieve the minimum value of a given column. 283 | * 284 | * @param string $column 285 | * @return float|int 286 | */ 287 | public function min($column) 288 | { 289 | return $this->aggregate(__FUNCTION__, $column); 290 | } 291 | 292 | /** 293 | * Retrieve the maximum value of a given column. 294 | * 295 | * @param string $column 296 | * @return float|int 297 | */ 298 | public function max($column) 299 | { 300 | return $this->aggregate(__FUNCTION__, $column); 301 | } 302 | 303 | /** 304 | * Alias to set the "offset" value of the query. 305 | * 306 | * @param int $value 307 | * @return $this 308 | */ 309 | public function skip($value) 310 | { 311 | return $this->offset($value); 312 | } 313 | 314 | /** 315 | * Set the "offset" value of the query. 316 | * 317 | * @param int|null $value 318 | * @return $this 319 | */ 320 | public function offset($value) 321 | { 322 | $this->offset = ! is_null($value) ? (int) $value : null; 323 | 324 | return $this; 325 | } 326 | 327 | /** 328 | * Execute the query and get the first result. 329 | * 330 | * @return object|null 331 | */ 332 | public function first() 333 | { 334 | $this->limit(1); 335 | 336 | $items = $this->get(); 337 | 338 | return reset($items) ?: null; 339 | } 340 | 341 | /** 342 | * Set the "limit" value of the query. 343 | * 344 | * @param int|null $value 345 | * @return $this 346 | */ 347 | public function limit($value) 348 | { 349 | $this->limit = ! is_null($value) ? (int) $value : null; 350 | 351 | return $this; 352 | } 353 | 354 | /** 355 | * Add an "or where" clause to the query. 356 | * 357 | * @param string $column 358 | * @param mixed $operator 359 | * @param mixed $value 360 | * @return $this 361 | */ 362 | public function orWhere($column, $operator = null, $value = null) 363 | { 364 | list($value, $operator) = $this->prepareValueAndOperator( 365 | $value, 366 | $operator, 367 | func_num_args() === 2 368 | ); 369 | 370 | return $this->where($column, $operator, $value, 'or'); 371 | } 372 | 373 | /** 374 | * Prepare the value and operator for a where clause. 375 | * 376 | * @param string|numeric $value 377 | * @param string $operator 378 | * @param bool $useDefault 379 | * @return array 380 | * 381 | * @throws InvalidArgumentException 382 | */ 383 | public function prepareValueAndOperator($value, $operator, $useDefault = false) 384 | { 385 | if ($useDefault) { 386 | return [$operator, '=']; 387 | } elseif ($this->invalidOperatorAndValue($operator, $value)) { 388 | throw new InvalidArgumentException('Illegal operator and value combination.'); 389 | } 390 | 391 | return [$value, $operator]; 392 | } 393 | 394 | /** 395 | * Determine if the given operator and value combination is legal. 396 | * 397 | * Prevents using Null values with invalid operators. 398 | * 399 | * @param string $operator 400 | * @param mixed $value 401 | * @return bool 402 | */ 403 | protected function invalidOperatorAndValue($operator, $value) 404 | { 405 | return is_null($value) && in_array($operator, $this->operators) && 406 | ! in_array($operator, ['=', '<>', '!=']); 407 | } 408 | 409 | /** 410 | * Add a basic where clause to the query. 411 | * 412 | * @param string $column 413 | * @param string|null $operator 414 | * @param string|null|float|int $value 415 | * @param string $boolean 416 | * @return $this 417 | */ 418 | public function where($column, $operator = null, $value = null, $boolean = 'and') 419 | { 420 | $type = 'Basic'; 421 | 422 | // Here we will make some assumptions about the operator. If only 2 values are 423 | // passed to the method, we will assume that the operator is an equals sign 424 | // and keep going. Otherwise, we'll require the operator to be passed in. 425 | list($value, $operator) = $this->prepareValueAndOperator( 426 | $value, // @phpstan-ignore-line 427 | $operator, // @phpstan-ignore-line 428 | func_num_args() === 2 429 | ); 430 | 431 | // If the value is "null", we will just assume the developer wants to add a 432 | // where null clause to the query. So, we will allow a short-cut here to 433 | // that method for convenience so the developer doesn't have to check. 434 | if (is_null($value)) { 435 | return $this->whereNull($column, $boolean, $operator !== '='); 436 | } 437 | 438 | $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean'); 439 | 440 | $this->addBinding($value); 441 | 442 | return $this; 443 | } 444 | 445 | /** 446 | * Add a "where null" clause to the query. 447 | * 448 | * @param string|array $columns 449 | * @param string $boolean 450 | * @param bool $not 451 | * @return $this 452 | */ 453 | public function whereNull($columns, $boolean = 'and', $not = false) 454 | { 455 | $type = $not ? 'NotNull' : 'Null'; 456 | 457 | foreach ((array) $columns as $column) { 458 | $this->wheres[] = compact('type', 'column', 'boolean'); 459 | } 460 | 461 | return $this; 462 | } 463 | 464 | /** 465 | * Add a binding to the query. 466 | * 467 | * @param mixed $value 468 | * @param string $type 469 | * @return $this 470 | * 471 | * @throws InvalidArgumentException 472 | */ 473 | protected function addBinding($value, $type = 'where') 474 | { 475 | if (! array_key_exists($type, $this->bindings)) { 476 | throw new InvalidArgumentException("Invalid binding type: $type."); 477 | } 478 | if (is_array($value)) { 479 | $this->bindings[$type] = array_merge($this->bindings[$type], $value); 480 | } else { 481 | $this->bindings[$type][] = $value; 482 | } 483 | 484 | return $this; 485 | } 486 | 487 | /** 488 | * Add a "where not in" clause to the query. 489 | * 490 | * @param string $column 491 | * @param mixed $values 492 | * @param string $boolean 493 | * @return $this 494 | */ 495 | public function whereNotIn($column, $values, $boolean = 'and') 496 | { 497 | return $this->whereIn($column, $values, $boolean, true); 498 | } 499 | 500 | /** 501 | * Add a "where in" clause to the query. 502 | * 503 | * @param string $column 504 | * @param mixed $values 505 | * @param string $boolean 506 | * @param bool $not 507 | * @return $this 508 | */ 509 | public function whereIn($column, $values, $boolean = 'and', $not = false) 510 | { 511 | $type = $not ? 'NotIn' : 'In'; 512 | 513 | $this->wheres[] = compact('type', 'column', 'values', 'boolean'); 514 | 515 | foreach ($values as $value) { 516 | $this->addBinding($value); 517 | } 518 | 519 | return $this; 520 | } 521 | 522 | /** 523 | * Add a "where not null" clause to the query. 524 | * 525 | * @param string|array $columns 526 | * @param string $boolean 527 | * @return $this 528 | */ 529 | public function whereNotNull($columns, $boolean = 'and') 530 | { 531 | return $this->whereNull($columns, $boolean, true); 532 | } 533 | 534 | /** 535 | * Add a where not between statement to the query. 536 | * 537 | * @param string $column 538 | * @param array $values 539 | * @param string $boolean 540 | * @return $this 541 | */ 542 | public function whereNotBetween($column, array $values, $boolean = 'and') 543 | { 544 | return $this->whereBetween($column, $values, $boolean, true); 545 | } 546 | 547 | /** 548 | * Add a where between statement to the query. 549 | * 550 | * @param string $column 551 | * @param array $values 552 | * @param string $boolean 553 | * @param bool $not 554 | * @return $this 555 | */ 556 | public function whereBetween($column, $values, $boolean = 'and', $not = false) 557 | { 558 | $type = 'Between'; 559 | 560 | $values = array_values(array_slice($values, 0, 2)); 561 | 562 | $this->wheres[] = compact('type', 'column', 'values', 'boolean', 'not'); 563 | 564 | foreach ($values as $value) { 565 | $this->addBinding($value); 566 | } 567 | 568 | return $this; 569 | } 570 | 571 | /** 572 | * Insert new records into the database. 573 | * 574 | * @param array $values 575 | * @param bool $ignore 576 | * @return bool|int|string 577 | */ 578 | public function insert(array $values, $ignore = false) 579 | { 580 | if (! empty($values) && ! is_array(reset($values))) { 581 | $values = [$values]; 582 | } 583 | 584 | $payload = array_reduce($values, function ($values, $value) { 585 | return array_merge($values, array_values($value)); 586 | }, []); 587 | 588 | $query = $this->grammar->compileInsert($this, $values, $ignore); 589 | 590 | if ($ignore) { 591 | return $this 592 | ->connection 593 | ->affectingStatement($query, $payload); 594 | } 595 | 596 | return $this 597 | ->connection 598 | ->insert($query, $payload); 599 | } 600 | 601 | 602 | /** 603 | * Insert new record and returns its ID 604 | * 605 | * @param array $values 606 | * @return int|string 607 | */ 608 | public function insertGetId(array $values) 609 | { 610 | return $this 611 | ->connection 612 | ->affectingStatement( 613 | $this->grammar->compileInsert($this, [$values], false), array_values($values), 614 | ReturnType::INSERT_ID 615 | ); 616 | } 617 | 618 | /** 619 | * Update records in the database. 620 | * 621 | * @param array $values 622 | * @return int|string 623 | */ 624 | public function update(array $values) 625 | { 626 | $bindings = array_merge(array_values($values), $this->getBindings()); 627 | 628 | return $this->connection->affectingStatement( 629 | $this->grammar->compileUpdate($this, $values), 630 | $bindings 631 | ); 632 | } 633 | 634 | /** 635 | * Delete records from the database. 636 | * 637 | * @return int|string 638 | */ 639 | public function delete() 640 | { 641 | return $this->connection->affectingStatement( 642 | $this->grammar->compileDelete($this), 643 | $this->getBindings() 644 | ); 645 | } 646 | 647 | /** 648 | * Add an "order by" clause to the query. 649 | * 650 | * @param string $column 651 | * @param string $direction 652 | * @return $this 653 | */ 654 | public function orderBy($column, $direction = 'asc') 655 | { 656 | $this->orders[] = compact('column', 'direction'); 657 | 658 | return $this; 659 | } 660 | 661 | /** 662 | * Add a "where" clause comparing two columns to the query. 663 | * 664 | * @param string $first 665 | * @param string|null $operator 666 | * @param string|null $second 667 | * @param string $boolean 668 | * @return $this 669 | */ 670 | public function whereColumn($first, $operator = null, $second = null, $boolean = 'and') 671 | { 672 | $type = 'Column'; 673 | 674 | list($second, $operator) = $this->prepareValueAndOperator( 675 | $second, // @phpstan-ignore-line 676 | $operator, // @phpstan-ignore-line 677 | func_num_args() === 2 678 | ); 679 | 680 | $this->wheres[] = compact('type', 'first', 'operator', 'second', 'boolean'); 681 | 682 | return $this; 683 | } 684 | 685 | /** 686 | * Add a left join clause to the query 687 | * 688 | * @param string $table 689 | * @param string|null $first 690 | * @param string|null $operator 691 | * @param string|null $second 692 | * @return $this 693 | */ 694 | public function leftJoin($table, $first = null, $operator = null, $second = null) 695 | { 696 | return $this->join($table, $first, $operator, $second, 'left'); 697 | } 698 | 699 | /** 700 | * Add a join clause to the query. 701 | * 702 | * @param string $table 703 | * @param string|null|Closure $first 704 | * @param string|null $operator 705 | * @param string|null $second 706 | * @param string $type 707 | * @return $this 708 | */ 709 | public function join($table, $first = null, $operator = null, $second = null, $type = 'inner') 710 | { 711 | $join = new Join($table, $type, $this->connection); 712 | 713 | if ($first instanceof Closure) { 714 | $first($join); 715 | } elseif (! is_null($first)) { 716 | $join->on($first, $operator, $second); 717 | } 718 | 719 | $this->addBinding($join->getBindings(), 'join'); 720 | 721 | $this->joins[] = $join; 722 | 723 | return $this; 724 | } 725 | 726 | /** 727 | * Add a right join clause to the query 728 | * 729 | * @param string $table 730 | * @param string|null|Closure $first 731 | * @param string|null $operator 732 | * @param string|null $second 733 | * @return $this 734 | */ 735 | public function rightJoin($table, $first = null, $operator = null, $second = null) 736 | { 737 | return $this->join($table, $first, $operator, $second, 'right'); 738 | } 739 | 740 | /** 741 | * Apply a mixin to builder class 742 | * 743 | * @return $this 744 | */ 745 | public function plugin(Pluggable $pluggable) 746 | { 747 | $pluggable->apply($this); 748 | 749 | return $this; 750 | } 751 | 752 | /** 753 | * Add a `group by` clause to query 754 | * 755 | * @param string $column 756 | * @param string|null $direction 757 | * @return Builder 758 | */ 759 | public function groupBy($column, $direction = null) 760 | { 761 | $this->groups[] = [$column, $direction]; 762 | 763 | return $this; 764 | } 765 | 766 | /** 767 | * Add a nested where statement to the query. 768 | * 769 | * @param string $boolean 770 | * @return $this 771 | */ 772 | public function whereNested(Closure $callback, $boolean = 'and') 773 | { 774 | $callback($query = $this->newQuery()); 775 | 776 | if (! empty($query->wheres)) { 777 | $type = 'Nested'; 778 | 779 | $this->wheres[] = compact('type', 'query', 'boolean'); 780 | 781 | foreach ($query->bindings['where'] as $binding) { 782 | $this->addBinding($binding); 783 | } 784 | } 785 | 786 | return $this; 787 | } 788 | 789 | /** 790 | * Get a new instance of the query builder. 791 | * 792 | * @return Builder 793 | */ 794 | public function newQuery() 795 | { 796 | return new static($this->connection, $this->grammar); 797 | } 798 | 799 | /** 800 | * Run a truncate statement on the table. 801 | * 802 | * @return bool 803 | */ 804 | public function truncate() 805 | { 806 | return $this->connection->statement( 807 | $this->grammar->compileTruncate($this) 808 | ); 809 | } 810 | 811 | /** 812 | * Add `withOne` relation 813 | * 814 | * @param string $name 815 | * @param string $foreignKey 816 | * @param string $localKey 817 | * @return Builder 818 | */ 819 | public function withOne($name, callable $callback, $foreignKey, $localKey = 'ID') 820 | { 821 | call_user_func($callback, $relation = new WithOne($name, $foreignKey, $localKey, $this->newQuery())); 822 | 823 | $this->with[] = $relation; 824 | 825 | return $this; 826 | } 827 | 828 | /** 829 | * Add `withMany` relation 830 | * 831 | * @param string $name 832 | * @param string $foreignKey 833 | * @param string $localKey 834 | * @return $this 835 | */ 836 | public function withMany($name, callable $callback, $foreignKey, $localKey = 'ID') 837 | { 838 | call_user_func($callback, $relation = new WithMany($name, $foreignKey, $localKey, $this->newQuery())); 839 | 840 | $this->with[] = $relation; 841 | 842 | return $this; 843 | } 844 | 845 | /** 846 | * Add relation to query 847 | * 848 | * @return $this 849 | */ 850 | public function withRelation(Relation $relation, callable $callback = null) 851 | { 852 | if (! is_null($callback)) { 853 | call_user_func($callback, $relation); 854 | } 855 | 856 | $this->with[] = $relation; 857 | 858 | return $this; 859 | } 860 | } 861 | -------------------------------------------------------------------------------- /src/Query/Grammar.php: -------------------------------------------------------------------------------- 1 | distinct ? ' distinct' : '')]; 60 | 61 | foreach ($this->selectComponents as $component) { 62 | if (isset($builder->{$component})) { 63 | $sql[$component] = call_user_func( 64 | [$this, 'compile'.ucfirst($component)], // @phpstan-ignore-line 65 | $builder, 66 | $builder->{$component} 67 | ); 68 | } 69 | } 70 | 71 | return implode(' ', array_filter($sql)); 72 | } 73 | 74 | /** 75 | * Compile an insert statement into SQL. 76 | * 77 | * @param array $values 78 | * @param bool $ignore 79 | * @return string 80 | */ 81 | public function compileInsert(Builder $builder, array $values, $ignore) 82 | { 83 | $table = $this->tableWithPrefix($builder->from); 84 | 85 | if (empty($values)) { 86 | return "insert into $table default values"; 87 | } 88 | 89 | $columns = $this->columnize(array_keys(reset($values))); // @phpstan-ignore-line 90 | 91 | $placeholderValues = implode(', ', array_map(function ($value) { 92 | return '('.implode(', ', array_map([$this, 'getValuePlaceholder'], $value)).')'; 93 | }, $values)); 94 | 95 | return ($ignore ? 'insert ignore' : 'insert')." into $table($columns) values $placeholderValues"; 96 | } 97 | 98 | /** 99 | * Get table name with prefix 100 | * 101 | * @param string $table 102 | * @return string 103 | */ 104 | protected function tableWithPrefix($table) 105 | { 106 | return $this->getTablePrefix().$table; 107 | } 108 | 109 | /** 110 | * Get the grammar's table prefix. 111 | * 112 | * @return string 113 | */ 114 | public function getTablePrefix() 115 | { 116 | return $this->tablePrefix; 117 | } 118 | 119 | /** 120 | * Set the grammar's table prefix. 121 | * 122 | * @param string $prefix 123 | * @return $this 124 | */ 125 | public function setTablePrefix($prefix) 126 | { 127 | $this->tablePrefix = $prefix; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Convert an array of column names into a delimited string. 134 | * 135 | * @param array $columns 136 | * @return string 137 | */ 138 | public function columnize(array $columns) 139 | { 140 | return implode(', ', $columns); 141 | } 142 | 143 | /** 144 | * Compile an update statement into SQL. 145 | * 146 | * @param array $values 147 | * @return string 148 | */ 149 | public function compileUpdate(Builder $builder, array $values) 150 | { 151 | $columns = $this->compileUpdateColumns($values); 152 | $where = $this->compileWheres($builder); 153 | 154 | return trim("update {$this->tableWithPrefix($builder->from)} set $columns $where"); 155 | } 156 | 157 | /** 158 | * Compile the columns for an update statement. 159 | * 160 | * @param array $values 161 | * @return string 162 | */ 163 | protected function compileUpdateColumns($values) 164 | { 165 | return implode(', ', array_map(function ($key) use (&$values) { 166 | return "$key = {$this->getValuePlaceholder($values[$key])}"; 167 | }, array_keys($values))); 168 | } 169 | 170 | /** 171 | * Get value placeholder based on value data type 172 | * 173 | * @param mixed|null $value 174 | * @return string 175 | */ 176 | protected function getValuePlaceholder($value) 177 | { 178 | if (is_null($value)) { 179 | return 'null'; 180 | } 181 | 182 | return '?'; 183 | } 184 | 185 | /** 186 | * Compile the "where" portions of the query. 187 | * 188 | * @return string 189 | */ 190 | public function compileWheres(Builder $builder) 191 | { 192 | if (empty($builder->wheres)) { 193 | return ''; 194 | } 195 | 196 | return $this->concatenateWhereClauses( 197 | $builder, 198 | $this->compileWheresToArray($builder) 199 | ); 200 | } 201 | 202 | /** 203 | * Format the where clause statements into one string. 204 | * 205 | * @param Builder $builder 206 | * @param array $whereSegment 207 | * @return string 208 | */ 209 | protected function concatenateWhereClauses($builder, $whereSegment) 210 | { 211 | return ($builder instanceof Join ? 'on' : 'where').' '.$this->removeLeadingBoolean( 212 | implode(' ', $whereSegment) 213 | ); 214 | } 215 | 216 | /** 217 | * Remove the leading boolean from a statement. 218 | * 219 | * @param string $value 220 | * @return string 221 | */ 222 | protected function removeLeadingBoolean($value) 223 | { 224 | return preg_replace('/and |or /i', '', $value, 1); // @phpstan-ignore-line 225 | } 226 | 227 | /** 228 | * Get an array of all the where clauses for the query. 229 | * 230 | * @return string[] 231 | */ 232 | protected function compileWheresToArray(Builder $builder) 233 | { 234 | return array_map(function ($where) use ($builder) { 235 | return $where['boolean'].' '.call_user_func([$this, 'where'.$where['type']], $builder, $where); // @phpstan-ignore-line 236 | }, $builder->wheres); 237 | } 238 | 239 | /** 240 | * Compile a delete statement into SQL. 241 | * 242 | * @return string 243 | */ 244 | public function compileDelete(Builder $builder) 245 | { 246 | $table = $this->tableWithPrefix($builder->from); 247 | $where = $this->compileWheres($builder); 248 | 249 | return trim("delete from $table $where"); 250 | } 251 | 252 | /** 253 | * Compile a truncate table statement into SQL. 254 | * 255 | * @return string 256 | */ 257 | public function compileTruncate(Builder $builder) 258 | { 259 | return 'truncate table '.$this->tableWithPrefix($builder->from); 260 | } 261 | 262 | /** 263 | * Compile an aggregated select clause. 264 | * 265 | * @param array $aggregate 266 | * @return string 267 | */ 268 | protected function compileAggregate(Builder $builder, array $aggregate) 269 | { 270 | return sprintf('%s(%s) as aggregate', $aggregate[0], $aggregate[1]); 271 | } 272 | 273 | /** 274 | * Compile the "select *" portion of the query. 275 | * 276 | * @param array|string $columns 277 | * @return bool|string|null 278 | */ 279 | protected function compileColumns(Builder $builder, $columns) 280 | { 281 | if (isset($builder->aggregate) && $columns === '*') { 282 | return null; 283 | } 284 | 285 | if (is_string($builder->distinct)) { 286 | return $builder->distinct; 287 | } 288 | 289 | return $this->withPrefixColumns($columns); 290 | } 291 | 292 | /** 293 | * Wrap with table prefix 294 | * 295 | * @param array|string $columns 296 | * @return string 297 | */ 298 | protected function withPrefixColumns($columns) 299 | { 300 | if (is_string($columns)) { 301 | $columns = [$columns]; 302 | } 303 | 304 | $columns = array_map(function ($column) { 305 | if (strpos($column, '.') === false) { 306 | return $column; 307 | } 308 | 309 | return $this->tableWithPrefix($column); 310 | }, $columns); 311 | 312 | return $this->columnize($columns); 313 | } 314 | 315 | /** 316 | * Compile the "from" portion of the query. 317 | * 318 | * @param string $from 319 | * @return string 320 | */ 321 | protected function compileFrom(Builder $builder, $from) 322 | { 323 | return 'from '.$this->tableWithPrefix($from); 324 | } 325 | 326 | /** 327 | * Compile the "limit" portions of the query. 328 | * 329 | * @param int $limit 330 | * @return string 331 | */ 332 | protected function compileLimit(Builder $builder, $limit) 333 | { 334 | return 'limit '.$limit; 335 | } 336 | 337 | /** 338 | * Compile the "offset" portions of the query. 339 | * 340 | * @param int $offset 341 | * @return string 342 | */ 343 | protected function compileOffset(Builder $builder, $offset) 344 | { 345 | return 'offset '.$offset; 346 | } 347 | 348 | /** 349 | * Compile basic where clause 350 | * 351 | * @param array $where 352 | * @return string 353 | */ 354 | protected function whereBasic(Builder $builder, $where) 355 | { 356 | return "{$this->withPrefixColumns($where['column'])} {$where['operator']} ".$this->getValuePlaceholder($where['value']); 357 | } 358 | 359 | /** 360 | * Compile a "where in" clause. 361 | * 362 | * @param array $where 363 | * @return string 364 | */ 365 | protected function whereIn(Builder $builder, $where) 366 | { 367 | if (! empty($where['values'])) { 368 | return $this->withPrefixColumns($where['column']).' in ('.implode(', ', array_map([$this, 'getValuePlaceholder'], $where['values'])).')'; 369 | } 370 | 371 | return '0 = 1'; 372 | } 373 | 374 | /** 375 | * Compile a "where not in" clause. 376 | * 377 | * @param array $where 378 | * @return string 379 | */ 380 | protected function whereNotIn(Builder $builder, $where) 381 | { 382 | if (! empty($where['values'])) { 383 | return $this->withPrefixColumns($where['column']).' not in ('.implode(', ', array_map([$this, 'getValuePlaceholder'], $where['values'])).')'; 384 | } 385 | 386 | return '1 = 1'; 387 | } 388 | 389 | /** 390 | * Compile a "where is null" clause. 391 | * 392 | * @param array $where 393 | * @return string 394 | */ 395 | protected function whereNull(Builder $builder, $where) 396 | { 397 | return $this->withPrefixColumns($where['column']).' is null'; 398 | } 399 | 400 | /** 401 | * Compile a "where is not null" clause. 402 | * 403 | * @param array $where 404 | * @return string 405 | */ 406 | protected function whereNotNull(Builder $builder, $where) 407 | { 408 | return $this->withPrefixColumns($where['column']).' is not null'; 409 | } 410 | 411 | /** 412 | * Compile a "between" where clause. 413 | * 414 | * @param array $where 415 | * @return string 416 | */ 417 | protected function whereBetween(Builder $builder, $where) 418 | { 419 | $between = $where['not'] ? 'not between' : 'between'; 420 | 421 | return $this->withPrefixColumns($where['column']).' '.$between.' '.implode(' and ', array_map([$this, 'getValuePlaceholder'], $where['values'])); 422 | } 423 | 424 | /** 425 | * Compile the "order by" portions of the query. 426 | * 427 | * @param array> $orders 428 | * @return string 429 | */ 430 | protected function compileOrders(Builder $builder, $orders) 431 | { 432 | return 'order by '.implode(', ', array_map(function ($order) { 433 | return "{$order['column']} {$order['direction']}"; 434 | }, $orders)); 435 | } 436 | 437 | /** 438 | * Compile a where clause comparing two columns. 439 | * 440 | * @param array $where 441 | * @return string 442 | */ 443 | protected function whereColumn(Builder $builder, array $where) 444 | { 445 | return "{$this->withPrefixColumns($where['first'])} {$where['operator']} {$this->withPrefixColumns($where['second'])}"; 446 | } 447 | 448 | /** 449 | * Compile the "join" portions of the query. 450 | * 451 | * @param array $joins 452 | * @return string 453 | */ 454 | protected function compileJoins(Builder $builder, array $joins) 455 | { 456 | return implode(' ', array_map(function (Join $join) use ($builder) { 457 | $nestedJoins = is_null($join->joins) ? '' : ' '.$this->compileJoins($builder, $join->joins); 458 | 459 | $tableAndNestedJoins = is_null($join->joins) ? $this->tableWithPrefix($join->table) : '('.$this->tableWithPrefix($join->table).$nestedJoins.')'; 460 | 461 | return "$join->type join $tableAndNestedJoins {$this->compileWheres($join)}"; 462 | }, $joins)); 463 | } 464 | 465 | /** 466 | * Compile the "group by" portions of the query. 467 | * 468 | * @param array> $groups 469 | * @return string 470 | */ 471 | protected function compileGroups(Builder $builder, $groups) 472 | { 473 | $groups = array_map(function ($group) { 474 | return implode(' ', array_filter($group)); 475 | }, $groups); 476 | 477 | return 'group by '.$this->columnize($groups); 478 | } 479 | 480 | /** 481 | * Compile a nested where clause. 482 | * 483 | * @param array $where 484 | * @return string 485 | */ 486 | protected function whereNested(Builder $builder, array $where) 487 | { 488 | // Here we will calculate what portion of the string we need to remove. If this 489 | // is a join clause query, we need to remove the "on" portion of the SQL and 490 | // if it is a normal query we need to take the leading "where" of queries. 491 | $offset = $builder instanceof Join ? 3 : 6; 492 | 493 | return '('.substr($this->compileWheres($where['query']), $offset).')'; 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /src/Query/Join.php: -------------------------------------------------------------------------------- 1 | table = $table; 32 | $this->type = $type; 33 | 34 | parent::__construct($connection, $grammar); 35 | } 36 | 37 | /** 38 | * Add an "or on" clause to the join. 39 | * 40 | * @param string $first 41 | * @param string|null $operator 42 | * @param string|null $second 43 | * @return Join 44 | */ 45 | public function orOn($first, $operator = null, $second = null) 46 | { 47 | return $this->on($first, $operator, $second, 'or'); 48 | } 49 | 50 | /** 51 | * Add an "on" clause to the join. 52 | * 53 | * @param string $first 54 | * @param string|null $operator 55 | * @param string|null $second 56 | * @param string $boolean 57 | * @return Join 58 | */ 59 | public function on($first, $operator = null, $second = null, $boolean = 'and') 60 | { 61 | return $this->whereColumn($first, $operator, $second, $boolean); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Query/ReturnType.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | protected $items; 33 | 34 | /** 35 | * Constructor of Relation Class 36 | * 37 | * @param string $name 38 | */ 39 | public function __construct($name, Builder $builder = null) 40 | { 41 | $this->name = $name; 42 | $this->builder = $builder ?: new Builder($this->builder->connection); 43 | } 44 | 45 | /** 46 | * Load related items 47 | * 48 | * @return array 49 | */ 50 | public function load() 51 | { 52 | $loadedItems = $this->loadedItemsDictionary(); 53 | 54 | return array_map(function ($item) use (&$loadedItems) { 55 | $item->{$this->name} = $this->getItemFromDictionary($loadedItems, $item); // @phpstan-ignore-line 56 | 57 | return $item; 58 | }, $this->items); 59 | } 60 | 61 | /** 62 | * Loaded items with under its foreign key 63 | * 64 | * @return array> 65 | */ 66 | abstract protected function loadedItemsDictionary(); 67 | 68 | /** 69 | * Get mapped value from dictionary 70 | * 71 | * @param array> $loadedItems 72 | * @param object $item 73 | * @return object|null | array 74 | */ 75 | abstract protected function getItemFromDictionary($loadedItems, $item); 76 | 77 | /** 78 | * Set items of record 79 | * 80 | * @param array $items 81 | * @return $this 82 | */ 83 | public function setItems(array $items) 84 | { 85 | $this->items = $items; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Handle dynamic methods call of query builder 92 | * 93 | * @param string $name 94 | * @param array $arguments 95 | * @return Builder 96 | */ 97 | public function __call($name, $arguments) 98 | { 99 | if ($name === 'get') { 100 | self::throwBadMethodCallException($name); 101 | } 102 | 103 | return self::forwardCallTo($this->builder, $name, $arguments); 104 | } 105 | 106 | /** 107 | * Get loaded items 108 | * 109 | * @return array 110 | */ 111 | abstract protected function getLoadedItems(); 112 | } 113 | -------------------------------------------------------------------------------- /src/Relations/WithMany.php: -------------------------------------------------------------------------------- 1 | > 11 | */ 12 | protected function loadedItemsDictionary() 13 | { 14 | $items = []; 15 | $loadedItems = $this->getLoadedItems(); 16 | 17 | foreach ($loadedItems as $loadedItem) { 18 | $items[$loadedItem->{$this->foreignKey}][] = $loadedItem; 19 | } 20 | 21 | return $items; // @phpstan-ignore-line 22 | } 23 | 24 | /** 25 | * Get mapped values from dictionary 26 | * 27 | * @param array> $loadedItems 28 | * @param object $item 29 | * @return array 30 | */ 31 | protected function getItemFromDictionary($loadedItems, $item) 32 | { 33 | if (array_key_exists($item->{$this->localKey}, $loadedItems)) { 34 | return $loadedItems[$item->{$this->localKey}]; 35 | } 36 | 37 | return []; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Relations/WithOne.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | protected function loadedItemsDictionary() 13 | { 14 | $items = []; 15 | $loadedItems = $this->getLoadedItems(); 16 | 17 | foreach ($loadedItems as $loadedItem) { 18 | $items[$loadedItem->{$this->foreignKey}] = $loadedItem; 19 | } 20 | 21 | return $items; 22 | } 23 | 24 | /** 25 | * Get mapped value from dictionary 26 | * 27 | * @return object|null 28 | */ 29 | protected function getItemFromDictionary($loadedItems, $item) 30 | { 31 | if (array_key_exists($item->{$this->localKey}, $loadedItems)) { 32 | return $loadedItems[$item->{$this->localKey}]; // @phpstan-ignore-line 33 | } 34 | 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Relations/WithOneOrMany.php: -------------------------------------------------------------------------------- 1 | foreignKey = $foreignKey; 31 | $this->localKey = $localKey; 32 | 33 | parent::__construct($name, $builder); 34 | } 35 | 36 | /** 37 | * Get loaded items 38 | * 39 | * @return array 40 | */ 41 | protected function getLoadedItems() 42 | { 43 | return $this->builder->whereIn($this->foreignKey, $this->extractKeyValues())->get(); 44 | } 45 | 46 | /** 47 | * Extract foreign key values 48 | * 49 | * @return array 50 | */ 51 | protected function extractKeyValues() 52 | { 53 | return array_column($this->items, $this->localKey); 54 | } 55 | } 56 | --------------------------------------------------------------------------------