├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── AddSubSelects.php └── ServiceProvider.php └── tests └── TestCase.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Loris Leiva 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Add Select 2 | 3 | 🧱 Add subSelect queries to your Laravel models using dynamic methods. 4 | 5 | If you're not familiar with subSelect queries, I strongly recommend [this article by Johnathan Reinink](https://reinink.ca/articles/dynamic-relationships-in-laravel-using-subqueries#can-this-be-done-with-a-has-one). 6 | 7 | ## Installation 8 | 9 | ```bash 10 | composer require lorisleiva/laravel-add-select 11 | ``` 12 | 13 | ## Usage 14 | 15 | Consider two Eloquent models `Book` and `Chapter` such that a book can have multiple chapters. 16 | 17 | By using the `AddSubSelects` trait, you can now add subSelect queries to the `Book` model by following the naming convention `add{NewColumnName}Select`. For example, the following piece of code add two new subSelect queries to the columns `last_chapter_id` and `latest_version`. 18 | 19 | ```php 20 | class Book extends Model 21 | { 22 | use AddSubSelects; 23 | 24 | public function addLastChapterIdSelect() 25 | { 26 | return Chapter::select('id') 27 | ->whereColumn('book_id', 'books.id') 28 | ->latest(); 29 | } 30 | 31 | public function addLatestVersionSelect() 32 | { 33 | return Chapter::select('version') 34 | ->whereColumn('book_id', 'books.id') 35 | ->orderByDesc('version'); 36 | } 37 | } 38 | ``` 39 | 40 | Now, you can eager-load these subSelect queries using the `withSelect` method. 41 | 42 | ```php 43 | Book::withSelect('last_chapter_id', 'latest_version')->get(); 44 | ``` 45 | 46 | You can also eager-load models that are already in memory using the `loadSelect` method. Note that this method will load all provided subSelect queries in one single database query. 47 | 48 | ```php 49 | $book->loadSelect('last_chapter_id', 'latest_version'); 50 | ``` 51 | 52 | If you haven't eager-loaded these subSelect queries in a model, you can still access them as attributes. The first time you access them, They will cause a new database query but the following times they will be available in the model's attributes. 53 | 54 | ```php 55 | $book->last_chapter_id; 56 | $book->latest_version; 57 | ``` 58 | 59 | Finally, you can gloabally eager-load these subSelect queries by setting up the `withSelect` property on the Eloquent model. 60 | 61 | ```php 62 | class Book extends Model 63 | { 64 | use AddSubSelects; 65 | 66 | public $withSelect = ['last_chapter_id', 'latest_version']; 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lorisleiva/laravel-add-select", 3 | "type": "library", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Loris Leiva", 8 | "email": "loris.leiva@gmail.com", 9 | "homepage": "https://lorisleiva.com" 10 | } 11 | ], 12 | "require": { 13 | "illuminate/support": "^5.8|^6.0|^7.0|^8.0" 14 | }, 15 | "require-dev": { 16 | "orchestra/testbench": "^5.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Lorisleiva\\LaravelAddSelect\\": "src" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "Lorisleiva\\LaravelAddSelect\\Tests\\": "tests" 26 | } 27 | }, 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "Lorisleiva\\LaravelAddSelect\\ServiceProvider" 32 | ] 33 | } 34 | }, 35 | "minimum-stability": "dev", 36 | "prefer-stable": true 37 | } 38 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | ./tests/Feature 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/AddSubSelects.php: -------------------------------------------------------------------------------- 1 | withSelect; 13 | static::addGlobalScope(function ($query) use ($withSelect) { 14 | $query->withSelect($withSelect); 15 | }); 16 | } 17 | } 18 | 19 | public function loadSelect($keys) 20 | { 21 | $keys = array_unique(is_string($keys) ? func_get_args() : $keys); 22 | 23 | // Prepare the wrapper query to attach subSelect queries. 24 | $wrapperQuery = $this->getSubSelectWrapperQuery(); 25 | 26 | // Add a subSelect query for each key that has a subSelect method 27 | // And keep track of those loaded keys in a different array. 28 | $loadedKeys = []; 29 | foreach ($keys as $key) { 30 | if ($query = $this->getSubSelectQuery($key)) { 31 | $wrapperQuery->selectSub($query->limit(1), $key); 32 | $loadedKeys[] = $key; 33 | } 34 | } 35 | 36 | // Assign attributes based on the wrapped query results for each loaded key. 37 | $result = $wrapperQuery->first(); 38 | foreach ($loadedKeys as $key) { 39 | $this->attributes[$key] = $result->{$key}; 40 | } 41 | 42 | return $this; 43 | } 44 | 45 | public function getAttribute($key) 46 | { 47 | if (! array_key_exists($key, $this->attributes) && 48 | ! $this->hasGetMutator($key) && 49 | $query = $this->getSubSelectQuery($key)) { 50 | return $this->attributes[$key] = $this->getValueFromSubSelectQuery($key, $query); 51 | } 52 | 53 | return parent::getAttribute($key); 54 | } 55 | 56 | public function getSubSelectQuery($key) 57 | { 58 | if (method_exists($this, $method = 'add'.Str::studly($key).'Select')) { 59 | return call_user_func([$this, $method]); 60 | } 61 | } 62 | 63 | protected function getSubSelectWrapperQuery() 64 | { 65 | return $this->query() 66 | ->getQuery() 67 | ->where($this->getKeyName(), $this->getKey()) 68 | ->limit(1); 69 | } 70 | 71 | protected function getValueFromSubSelectQuery($key, $query) 72 | { 73 | return $this->getSubSelectWrapperQuery() 74 | ->selectSub($query->limit(1), $key) 75 | ->first() 76 | ->{$key}; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | model))) { 18 | $classname = class_basename(get_class($this->model)); 19 | throw Exception("Model [$classname] does not use the AddSubSelects trait."); 20 | } 21 | 22 | // Ensure query columns are initialised. 23 | if (is_null($this->query->columns)) { 24 | $this->query->select($this->query->from.'.*'); 25 | } 26 | 27 | // Add a selectSub query for each key. 28 | foreach ((is_string($keys) ? func_get_args() : $keys) as $key) { 29 | if ($subQuery = $this->model->getSubSelectQuery($key)) { 30 | $this->query->selectSub($subQuery->limit(1), $key); 31 | } 32 | } 33 | 34 | return $this; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |