[增添]添加了datasource的setting数据库以及默认值

This commit is contained in:
makotocc0107
2024-08-27 09:57:44 +08:00
parent d111dfaea4
commit 72eb990970
10955 changed files with 978898 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
name: run-tests
on:
push:
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: [8.0, 8.1, 8.2, 8.3]
laravel: [8.*, 9.*, 10.*, 11.*]
include:
- laravel: 11.*
testbench: 9.*-dev
- laravel: 10.*
testbench: 8.*
- laravel: 9.*
testbench: 7.*
- laravel: 8.*
testbench: 6.*
exclude:
- laravel: 11.*
php: 8.0
- laravel: 11.*
php: 8.1
- laravel: 10.*
php: 8.0
- laravel: 8.*
php: 8.1
- laravel: 8.*
php: 8.2
- laravel: 9.*
php: 8.2
- laravel: 8.*
php: 8.3
- laravel: 9.*
php: 8.3
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Install SQLite 3
run: |
sudo apt-get update
sudo apt-get install sqlite3
- name: Cache dependencies
uses: actions/cache@v1
with:
path: ~/.composer/cache/files
key: dependencies-pw-v2-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv
coverage: none
tools: composer:v2
- name: Install dependencies
run: |
composer --version
composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --dev
composer update --prefer-dist --no-interaction --no-suggest --dev
composer dump
- name: Execute tests
run: vendor/bin/phpunit

View File

@@ -0,0 +1,100 @@
<?php
$finder = PhpCsFixer\Finder::create()
->exclude('vendor')
->in(__DIR__ . '/src')
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return PhpCsFixer\Config::create()
->setFinder($finder)
->setRules([
'@PSR2' => true,
'phpdoc_no_empty_return' => false,
'phpdoc_var_annotation_correct_order' => true,
'array_syntax' => [
'syntax' => 'short',
],
'no_singleline_whitespace_before_semicolons' => true,
'no_extra_blank_lines' => [
'break', 'case', 'continue', 'curly_brace_block', 'default',
'extra', 'parenthesis_brace_block', 'return',
'square_brace_block', 'switch', 'throw', 'use', 'useTrait', 'use_trait',
],
'cast_spaces' => [
'space' => 'single',
],
'single_quote' => true,
'lowercase_cast' => true,
'lowercase_static_reference' => true,
'no_empty_phpdoc' => true,
'no_empty_comment' => true,
'array_indentation' => true,
'short_scalar_cast' => true,
'no_mixed_echo_print' => [
'use' => 'echo',
],
'ordered_imports' => [
'sort_algorithm' => 'alpha',
],
'no_unused_imports' => true,
'binary_operator_spaces' => [
'default' => 'single_space',
],
'no_empty_statement' => true,
'unary_operator_spaces' => true, // $number ++ becomes $number++
'hash_to_slash_comment' => true, // # becomes //
'standardize_not_equals' => true, // <> becomes !=
'native_function_casing' => true,
'ternary_operator_spaces' => true,
'ternary_to_null_coalescing' => true,
'declare_equal_normalize' => [
'space' => 'single',
],
'function_typehint_space' => true,
'no_leading_import_slash' => true,
'blank_line_before_statement' => [
'statements' => [
'break', 'case', 'continue',
'declare', 'default', 'die',
'do', 'exit', 'for', 'foreach',
'goto', 'if', 'include',
'include_once', 'require', 'require_once',
'return', 'switch', 'throw', 'try', 'while', 'yield',
],
],
'combine_consecutive_unsets' => true,
'method_chaining_indentation' => true,
'no_whitespace_in_blank_line' => true,
'blank_line_after_opening_tag' => true,
'no_trailing_comma_in_list_call' => true,
'list_syntax' => ['syntax' => 'short'],
// public function getTimezoneAttribute( ? Banana $value) becomes public function getTimezoneAttribute(?Banana $value)
'compact_nullable_typehint' => true,
'explicit_string_variable' => true,
'no_leading_namespace_whitespace' => true,
'trailing_comma_in_multiline_array' => true,
'not_operator_with_successor_space' => true,
'object_operator_without_whitespace' => true,
'single_blank_line_before_namespace' => true,
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_whitespace_before_comma_in_array' => true,
'no_trailing_comma_in_singleline_array' => true,
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line',
],
'no_multiline_whitespace_around_double_arrow' => true,
'no_useless_return' => true,
'phpdoc_add_missing_param_annotation' => false,
'phpdoc_order' => true,
'phpdoc_scalar' => false,
'phpdoc_separation' => false,
'phpdoc_single_line_var_spacing' => false,
'single_trait_insert_per_statement' => true,
'return_type_declaration' => [
'space_before' => 'none',
],
])
->setLineEnding("\n");

View File

@@ -0,0 +1,27 @@
# Changelog
All notable changes to `eloquent-power-joins` will be documented in this file.
## 2.2.2 - 2020-10
- Fixed the ability to pass nested closures in join callbacks when using aliases;
## 2.2.1 - 2020-10
- Fixed nested conditions in relationship definitions;
## 2.1.0 - 2020-09
- Added the ability to include trashed models in join clauses;
## 2.0.0 - 2020-09
- Introduced trait that has to be used by models;
- Automatically applying extra relationship conditions;
- Ability to order by using left joins;
- Laravel 8 support;
_ Lots of bugfixes;
- Changed the method signature for sorting;
- Changed the method signature for querying relationship existence;
## 1.1.0
- Added the ability to use table aliases;
## 1.0.0
- Initial release;

View File

@@ -0,0 +1,55 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
Please read and understand the contribution guide before creating an issue or pull request.
## Etiquette
This project is open source, and as such, the maintainers give their free time to build and maintain the source code
held within. They make the code freely available in the hope that it will be of use to other developers. It would be
extremely unfair for them to suffer abuse or anger for their hard work.
Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
world that developers are civilized and selfless people.
It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
## Viability
When requesting or submitting new features, first consider whether it might be useful to others. Open
source projects are used by many developers, who may have entirely different needs to your own. Think about
whether or not your feature is likely to be used by other users of the project.
## Procedure
Before filing an issue:
- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
- Check to make sure your feature suggestion isn't already present within the project.
- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
- Check the pull requests tab to ensure that the feature isn't already in progress.
Before submitting a pull request:
- Check the codebase to ensure that your feature doesn't already exist.
- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
## Requirements
If the project maintainer has any additional requirements, you will find them listed here.
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
**Happy coding**!

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Luis Dalmolin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,351 @@
![Eloquent Power Joins](screenshots/eloquent-power-joins.jpg "Eloquent Power Joins")
![Laravel Supported Versions](https://img.shields.io/badge/laravel-8.x/9.x/10.x/11.x-green.svg)
[![run-tests](https://github.com/kirschbaum-development/eloquent-power-joins/actions/workflows/ci.yaml/badge.svg)](https://github.com/kirschbaum-development/eloquent-power-joins/actions/workflows/ci.yaml)
[![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
[![Latest Version on Packagist](https://img.shields.io/packagist/v/kirschbaum-development/eloquent-power-joins.svg?style=flat-square)](https://packagist.org/packages/kirschbaum-development/eloquent-power-joins)
[![Total Downloads](https://img.shields.io/packagist/dt/kirschbaum-development/eloquent-power-joins.svg?style=flat-square)](https://packagist.org/packages/kirschbaum-development/eloquent-power-joins)
The Laravel magic you know, now applied to joins.
Joins are very useful in a lot of ways. If you are here, you most likely know about and use them. Eloquent is very powerful, but it lacks a bit of the "Laravel way" when using joins. This package make your joins in a more Laravel way, with more readable with less code while hiding implementation details from places they don't need to be exposed.
A few things we consider is missing when using joins which are very powerful Eloquent features:
* Ability to use relationship definitions to make joins;
* Ability to use model scopes inside different contexts;
* Ability to query relationship existence using joins instead of where exists;
* Ability to easily sort results based on columns or aggregations from related tables;
You can read a more detailed explanation on the problems this package solves on [this blog post](https://kirschbaumdevelopment.com/news-articles/adding-some-laravel-magic-to-your-eloquent-joins).
## Installation
You can install the package via composer:
```bash
composer require kirschbaum-development/eloquent-power-joins
```
For Laravel versions < 8, use the 2.* version:
```bash
composer require kirschbaum-development/eloquent-power-joins:2.*
```
## Usage
This package provides a few features.
### 1 - Join Relationship
Let's say you have a `User` model with a `hasMany` relationship to the `Post` model. If you want to join the tables, you would usually write something like:
```php
User::select('users.*')->join('posts', 'posts.user_id', '=', 'users.id');
```
This package provides you with a new `joinRelationship()` method, which does the exact same thing.
```php
User::joinRelationship('posts');
```
Both options produce the same results. In terms of code, you didn't save THAT much, but you are now using the relationship between the `User` and the `Post` models to join the tables. This means that you are now hiding how this relationship works behind the scenes (implementation details). You also don't need to change the code if the relationship type changes. You now have more readable and less overwhelming code.
But, **it gets better** when you need to **join nested relationships**. Let's assume you also have a `hasMany` relationship between the `Post` and `Comment` models and you need to join these tables, you can simply write:
```php
User::joinRelationship('posts.comments');
```
So much better, wouldn't you agree?! You can also `left` or `right` join the relationships as needed.
```php
User::leftJoinRelationship('posts.comments');
User::rightJoinRelationship('posts.comments');
```
#### Joining polymorphic relationships
Let's imagine, you have a `Image` model that is a polymorphic relationship (`Post -> morphMany -> Image`). Besides the regular join, you would also need to apply the `where imageable_type = Post::class` condition, otherwise you could get messy results.
Turns out, if you join a polymorphic relationship, Eloquent Power Joins automatically applies this condition for you. You simply need to call the same method.
```php
Post::joinRelationship('images');
```
You can also join MorphTo relationships.
```php
Image::joinRelationship('imageable', morphable: Post::class);
```
Note: Querying morph to relationships only supports one morphable type at a time.
**Applying conditions & callbacks to the joins**
Now, let's say you want to apply a condition to the join you are making. You simply need to pass a callback as the second parameter to the `joinRelationship` method.
```php
User::joinRelationship('posts', fn ($join) => $join->where('posts.approved', true))->toSql();
```
For **nested calls**, you simply need to pass an array referencing the relationship names.
```php
User::joinRelationship('posts.comments', [
'posts' => fn ($join) => $join->where('posts.published', true),
'comments' => fn ($join) => $join->where('comments.approved', true),
]);
```
For **belongs to many** calls, you need to pass an array with the relationship, and then an array with the table names.
```php
User::joinRelationship('groups', [
'groups' => [
'groups' => function ($join) {
// ...
},
// group_members is the intermediary table here
'group_members' => fn ($join) => $join->where('group_members.active', true),
]
]);
```
#### Using model scopes inside the join callbacks 🤯
We consider this one of the most useful features of this package. Let's say, you have a `published` scope on your `Post` model:
```php
public function scopePublished($query)
{
$query->where('published', true);
}
```
When joining relationships, you **can** use the scopes defined in the model being joined. How cool is this?
```php
User::joinRelationship('posts', function ($join) {
// the $join instance here can access any of the scopes defined in Post 🤯
$join->published();
});
```
When using model scopes inside a join clause, you **can't** type hint the `$query` parameter in your scope. Also, keep in mind you are inside a join, so you are limited to use only conditions supported by joins.
#### Using aliases
Sometimes, you are going to need to use table aliases on your joins because you are joining the same table more than once. One option to accomplish this is to use the `joinRelationshipUsingAlias` method.
```php
Post::joinRelationshipUsingAlias('category.parent')->get();
```
In case you need to specify the name of the alias which is going to be used, you can do in two different ways:
1. Passing a string as the second parameter (this won't work for nested joins):
```php
Post::joinRelationshipUsingAlias('category', 'category_alias')->get();
```
2. Calling the `as` function inside the join callback.
```php
Post::joinRelationship('category.parent', [
'category' => fn ($join) => $join->as('category_alias'),
'parent' => fn ($join) => $join->as('category_parent'),
])->get()
```
For *belongs to many* or *has many through* calls, you need to pass an array with the relationship, and then an array with the table names.
```php
Group::joinRelationship('posts.user', [
'posts' => [
'posts' => fn ($join) => $join->as('posts_alias'),
'post_groups' => fn ($join) => $join->as('post_groups_alias'),
],
])->toSql();
```
#### Select * from table
When making joins, using `select * from ...` can be dangerous as fields with the same name between the parent and the joined tables could conflict. Thinking on that, if you call the `joinRelationship` method without previously selecting any specific columns, Eloquent Power Joins will automatically include that for you. For instance, take a look at the following examples:
```php
User::joinRelationship('posts')->toSql();
// select users.* from users inner join posts on posts.user_id = users.id
```
And, if you specify the select statement:
```php
User::select('users.id')->joinRelationship('posts')->toSql();
// select users.id from users inner join posts on posts.user_id = users.id
```
#### Soft deletes
When joining any models which uses the `SoftDeletes` trait, the following condition will be also automatically applied to all your joins:
```sql
and "users"."deleted_at" is null
```
In case you want to include trashed models, you can call the `->withTrashed()` method in the join callback.
```php
UserProfile::joinRelationship('users', fn ($join) => $join->withTrashed());
```
You can also call the `onlyTrashed` model as well:
```php
UserProfile::joinRelationship('users', ($join) => $join->onlyTrashed());
```
#### Extra conditions defined in relationships
If you have extra conditions in your relationship definitions, they will get automatically applied for you.
```php
class User extends Model
{
public function publishedPosts()
{
return $this->hasMany(Post::class)->published();
}
}
```
If you call `User::joinRelationship('publishedPosts')->get()`, it will also apply the additional published scope to the join clause. It would produce an SQL more or less like this:
```sql
select users.* from users inner join posts on posts.user_id = posts.id and posts.published = 1
```
#### Global Scopes
If your model have global scopes applied to it, you can enable the global scopes by calling the `withGlobalScopes` method in your join clause, like this:
```php
UserProfile::joinRelationship('users', fn ($join) => $join->withGlobalScopes());
```
There's, though, a gotcha here. Your global scope **cannot** type-hint the `Eloquent\Builder` class in the first parameter of the `apply` method, otherwise you will get errors.
### 2 - Querying relationship existence (Using Joins)
[Querying relationship existence](https://laravel.com/docs/7.x/eloquent-relationships#querying-relationship-existence) is a very powerful and convenient feature of Eloquent. However, it uses the `where exists` syntax which is not always the best and may not be the more performant choice, depending on how many records you have or the structure of your tables.
This packages implements the same functionality, but instead of using the `where exists` syntax, it uses **joins**. Below, you can see the methods this package implements and also the Laravel equivalent.
Please note that although the methods are similar, you will not always get the same results when using joins, depending on the context of your query. You should be aware of the differences between querying the data with `where exists` vs `joins`.
**Laravel Native Methods**
``` php
User::has('posts');
User::has('posts.comments');
User::has('posts', '>', 3);
User::whereHas('posts', fn ($query) => $query->where('posts.published', true));
User::whereHas('posts.comments', ['posts' => fn ($query) => $query->where('posts.published', true));
User::doesntHave('posts');
```
**Package equivalent, but using joins**
```php
User::powerJoinHas('posts');
User::powerJoinHas('posts.comments');
User::powerJoinHas('posts.comments', '>', 3);
User::powerJoinWhereHas('posts', function ($join) {
$join->where('posts.published', true);
});
User::powerJoinDoesntHave('posts');
```
When using the `powerJoinWhereHas` method with relationships that involves more than 1 table (One to Many, Many to Many, etc.), use the array syntax to pass the callback:
```php
User::powerJoinWhereHas('commentsThroughPosts', [
'comments' => fn ($query) => $query->where('body', 'a')
])->get());
```
### 3 - Order by
You can also sort your query results using a column from another table using the `orderByPowerJoins` method.
```php
User::orderByPowerJoins('profile.city');
```
If you need to pass some raw values for the order by function, you can do like this:
```php
User::orderByPowerJoins(['profile', DB::raw('concat(city, ", ", state)']);
```
This query will sort the results based on the `city` column on the `user_profiles` table. You can also sort your results by aggregations (`COUNT`, `SUM`, `AVG`, `MIN` or `MAX`).
For instance, to sort users with the highest number of posts, you can do this:
```php
$users = User::orderByPowerJoinsCount('posts.id', 'desc')->get();
```
Or, to get the list of posts where the comments contain the highest average of votes.
```php
$posts = Post::orderByPowerJoinsAvg('comments.votes', 'desc')->get();
```
You also have methods for `SUM`, `MIN` and `MAX`:
```php
Post::orderByPowerJoinsSum('comments.votes');
Post::orderByPowerJoinsMin('comments.votes');
Post::orderByPowerJoinsMax('comments.votes');
```
In case you want to use left joins in sorting, you also can:
```php
Post::orderByLeftPowerJoinsCount('comments.votes');
Post::orderByLeftPowerJoinsAvg('comments.votes');
Post::orderByLeftPowerJoinsSum('comments.votes');
Post::orderByLeftPowerJoinsMin('comments.votes');
Post::orderByLeftPowerJoinsMax('comments.votes');
```
***
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
### Security
If you discover any security related issues, please email luis@kirschbaumdevelopment.com or nathan@kirschbaumdevelopment.com instead of using the issue tracker.
## Credits
- [Luis Dalmolin](https://github.com/luisdalmolin)
## Sponsorship
Development of this package is sponsored by Kirschbaum Development Group, a developer driven company focused on problem solving, team building, and community. Learn more [about us](https://kirschbaumdevelopment.com) or [join us](https://careers.kirschbaumdevelopment.com)!
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
## Laravel Package Boilerplate
This package was generated using the [Laravel Package Boilerplate](https://laravelpackageboilerplate.com).

View File

@@ -0,0 +1,55 @@
{
"name": "kirschbaum-development/eloquent-power-joins",
"description": "The Laravel magic applied to joins.",
"keywords": [
"laravel",
"eloquent",
"mysql",
"join"
],
"homepage": "https://github.com/kirschbaum-development/eloquent-power-joins",
"license": "MIT",
"type": "library",
"authors": [
{
"name": "Luis Dalmolin",
"email": "luis.nh@gmail.com",
"role": "Developer"
}
],
"require": {
"php": "^8.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0",
"illuminate/database": "^8.0|^9.0|^10.0|^11.0"
},
"require-dev": {
"laravel/legacy-factories": "^1.0@dev",
"orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0",
"phpunit/phpunit": "^8.0|^9.0|^10.0"
},
"autoload": {
"psr-4": {
"Kirschbaum\\PowerJoins\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Kirschbaum\\PowerJoins\\Tests\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"providers": [
"Kirschbaum\\PowerJoins\\PowerJoinsServiceProvider"
]
}
},
"minimum-stability": "dev"
}

View File

@@ -0,0 +1,8 @@
<?php
/*
* You can place your custom package configuration in here.
*/
return [
//
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,26 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Database\Eloquent\Builder as EloquentQueryBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Kirschbaum\PowerJoins\Mixins\JoinRelationship;
use Kirschbaum\PowerJoins\Mixins\QueryBuilderExtraMethods;
use Kirschbaum\PowerJoins\Mixins\QueryRelationshipExistence;
use Kirschbaum\PowerJoins\Mixins\RelationshipsExtraMethods;
class EloquentJoins
{
/**
* Register macros with Eloquent.
*/
public static function registerEloquentMacros()
{
EloquentQueryBuilder::mixin(new JoinRelationship);
EloquentQueryBuilder::mixin(new QueryRelationshipExistence);
QueryBuilder::mixin(new QueryBuilderExtraMethods);
Relation::mixin(new RelationshipsExtraMethods);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Kirschbaum\PowerJoins;
/**
* @method static as(string $alias)
*/
class FakeJoinCallback extends PowerJoinClause
{
public function getAlias(): ?string
{
return $this->alias;
}
public function __call($name, $arguments)
{
if ($name === 'as') {
$this->alias = $arguments[0];
}
return $this;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\Relation;
class JoinsHelper
{
static array $instances = [];
protected function __construct()
{
}
public static function make(): static
{
$objects = array_map(fn ($object) => spl_object_id($object), func_get_args());
return static::$instances[implode('-', $objects)] ??= new self();
}
/**
* Cache to not join the same relationship twice.
*
* @var array
*/
private array $joinRelationshipCache = [];
/**
* Join method map.
*/
public static $joinMethodsMap = [
'join' => 'powerJoin',
'leftJoin' => 'leftPowerJoin',
'rightJoin' => 'rightPowerJoin',
];
/**
* Format the join callback.
*
* @param mixed $callback
* @return mixed
*/
public function formatJoinCallback($callback)
{
if (is_string($callback)) {
return function ($join) use ($callback) {
$join->as($callback);
};
}
return $callback;
}
public function generateAliasForRelationship(Relation $relation, string $relationName): array|string
{
if ($relation instanceof BelongsToMany || $relation instanceof HasManyThrough) {
return [
md5($relationName.'table1'.time()),
md5($relationName.'table2'.time()),
];
}
return md5($relationName.time());
}
/**
* Get the join alias name from all the different options.
*/
public function getAliasName(bool $useAlias, Relation $relation, string $relationName, string $tableName, $callback): null|string|array
{
if ($callback) {
if (is_callable($callback)) {
$fakeJoinCallback = new FakeJoinCallback($relation->getBaseQuery(), 'inner', $tableName);
$callback($fakeJoinCallback);
if ($fakeJoinCallback->getAlias()) {
return $fakeJoinCallback->getAlias();
}
}
if (is_array($callback) && isset($callback[$tableName])) {
$fakeJoinCallback = new FakeJoinCallback($relation->getBaseQuery(), 'inner', $tableName);
$callback[$tableName]($fakeJoinCallback);
if ($fakeJoinCallback->getAlias()) {
return $fakeJoinCallback->getAlias();
}
}
}
return $useAlias
? $this->generateAliasForRelationship($relation, $relationName)
: null;
}
/**
* Checks if the relationship was already joined.
*/
public function relationshipAlreadyJoined($model, string $relation): bool
{
return isset($this->joinRelationshipCache[spl_object_id($model)][$relation]);
}
/**
* Marks the relationship as already joined.
*/
public function markRelationshipAsAlreadyJoined($model, string $relation): void
{
$this->joinRelationshipCache[spl_object_id($model)][$relation] = true;
}
public function clear(): void
{
$this->joinRelationshipCache = [];
}
}

View File

@@ -0,0 +1,560 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Kirschbaum\PowerJoins\JoinsHelper;
use Kirschbaum\PowerJoins\PowerJoinClause;
use Kirschbaum\PowerJoins\StaticCache;
/**
* @mixin Builder
* @method \Illuminate\Database\Eloquent\Model getModel()
* @property \Illuminate\Database\Eloquent\Builder $query
*/
class JoinRelationship
{
/**
* New clause for making joins, where we pass the model to the joiner class.
*/
public function powerJoin(): Closure
{
return function ($table, $first, $operator = null, $second = null, $type = 'inner', $where = false): static {
$model = $operator instanceof Model ? $operator : null;
$join = $this->newPowerJoinClause($this->query, $type, $table, $model);
// If the first "column" of the join is really a Closure instance the developer
// is trying to build a join with a complex "on" clause containing more than
// one condition, so we'll add the join and call a Closure with the query.
if ($first instanceof Closure) {
$first($join);
$this->query->joins[] = $join;
$this->query->addBinding($join->getBindings(), 'join');
}
// If the column is simply a string, we can assume the join simply has a basic
// "on" clause with a single condition. So we will just build the join with
// this simple join clauses attached to it. There is not a join callback.
else {
$method = $where ? 'where' : 'on';
$this->query->joins[] = $join->$method($first, $operator, $second);
$this->query->addBinding($join->getBindings(), 'join');
}
return $this;
};
}
/**
* New clause for making joins, where we pass the model to the joiner class.
*/
public function leftPowerJoin(): Closure
{
return function ($table, $first, $operator = null, $second = null) {
return $this->powerJoin($table, $first, $operator, $second, 'left');
};
}
/**
* New clause for making joins, where we pass the model to the joiner class.
*/
public function rightPowerJoin(): Closure
{
return function ($table, $first, $operator = null, $second = null) {
return $this->powerJoin($table, $first, $operator, $second, 'right');
};
}
public function newPowerJoinClause(): Closure
{
return function (QueryBuilder $parentQuery, $type, $table, Model $model = null) {
return new PowerJoinClause($parentQuery, $type, $table, $model);
};
}
/**
* Join the relationship(s).
*/
public function joinRelationship(): Closure
{
return function (
$relationName,
$callback = null,
$joinType = 'join',
$useAlias = false,
bool $disableExtraConditions = false,
string $morphable = null
) {
$joinType = JoinsHelper::$joinMethodsMap[$joinType] ?? $joinType;
$useAlias = is_string($callback) ? false : $useAlias;
$joinHelper = JoinsHelper::make($this->getModel());
$callback = $joinHelper->formatJoinCallback($callback);
$this->getQuery()->beforeQuery(function () use ($joinHelper) {
$joinHelper->clear();
});
if (is_null($this->getSelect())) {
$this->select(sprintf('%s.*', $this->getModel()->getTable()));
}
if (Str::contains($relationName, '.')) {
$this->joinNestedRelationship($relationName, $callback, $joinType, $useAlias, $disableExtraConditions);
return $this;
}
$relationCallback = $callback;
if ($callback && is_array($callback) && isset($callback[$relationName]) && is_array($callback[$relationName])) {
$relationCallback = $callback[$relationName];
}
$relation = $this->getModel()->{$relationName}();
$relationQuery = $relation->getQuery();
$alias = $joinHelper->getAliasName(
$useAlias,
$relation,
$relationName,
$relationQuery->getModel()->getTable(),
$relationCallback
);
if ($relation instanceof BelongsToMany && !is_array($alias)) {
$extraAlias = $joinHelper->getAliasName(
$useAlias,
$relation,
$relationName,
$relation->getTable(),
$relationCallback
);
$alias = [$extraAlias, $alias];
}
$aliasString = is_array($alias) ? implode('.', $alias) : $alias;
$useAlias = $alias ? true : $useAlias;
$relationJoinCache = $alias
? "{$aliasString}.{$relationQuery->getModel()->getTable()}.{$relationName}"
: "{$relationQuery->getModel()->getTable()}.{$relationName}";
if ($joinHelper->relationshipAlreadyJoined($this->getModel(), $relationJoinCache)) {
return $this;
}
if ($useAlias) {
StaticCache::setTableAliasForModel($relation->getModel(), $alias);
}
$joinHelper->markRelationshipAsAlreadyJoined($this->getModel(), $relationJoinCache);
StaticCache::clear();
$relation->performJoinForEloquentPowerJoins(
builder: $this,
joinType: $joinType,
callback: $relationCallback,
alias: $alias,
disableExtraConditions: $disableExtraConditions,
morphable: $morphable,
);
return $this;
};
}
/**
* Join the relationship(s) using table aliases.
*/
public function joinRelationshipUsingAlias(): Closure
{
return function ($relationName, $callback = null, bool $disableExtraConditions = false) {
return $this->joinRelationship($relationName, $callback, 'join', true, $disableExtraConditions);
};
}
/**
* Left join the relationship(s) using table aliases.
*/
public function leftJoinRelationshipUsingAlias(): Closure
{
return function ($relationName, $callback = null, bool $disableExtraConditions = false) {
return $this->joinRelationship($relationName, $callback, 'leftJoin', true, $disableExtraConditions);
};
}
/**
* Right join the relationship(s) using table aliases.
*/
public function rightJoinRelationshipUsingAlias(): Closure
{
return function ($relationName, $callback = null, bool $disableExtraConditions = false) {
return $this->joinRelationship($relationName, $callback, 'rightJoin', true, $disableExtraConditions);
};
}
public function joinRelation(): Closure
{
return function (
$relationName,
$callback = null,
$joinType = 'join',
$useAlias = false,
bool $disableExtraConditions = false
) {
return $this->joinRelationship($relationName, $callback, $joinType, $useAlias, $disableExtraConditions);
};
}
public function leftJoinRelationship(): Closure
{
return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) {
return $this->joinRelationship($relation, $callback, 'leftJoin', $useAlias, $disableExtraConditions);
};
}
public function leftJoinRelation(): Closure
{
return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) {
return $this->joinRelationship($relation, $callback, 'leftJoin', $useAlias, $disableExtraConditions);
};
}
public function rightJoinRelationship(): Closure
{
return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) {
return $this->joinRelationship($relation, $callback, 'rightJoin', $useAlias, $disableExtraConditions);
};
}
public function rightJoinRelation(): Closure
{
return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) {
return $this->joinRelationship($relation, $callback, 'rightJoin', $useAlias, $disableExtraConditions);
};
}
/**
* Join nested relationships.
*/
public function joinNestedRelationship(): Closure
{
return function (
string $relationships,
$callback = null,
$joinType = 'join',
$useAlias = false,
bool $disableExtraConditions = false
) {
$relations = explode('.', $relationships);
$joinHelper = JoinsHelper::make($this->getModel());
/** @var Relation */
$latestRelation = null;
$part = [];
foreach ($relations as $relationName) {
$part[] = $relationName;
$fullRelationName = join(".", $part);
$currentModel = $latestRelation ? $latestRelation->getModel() : $this->getModel();
$relation = $currentModel->{$relationName}();
$relationCallback = null;
if ($callback && is_array($callback) && isset($callback[$relationName])) {
$relationCallback = $callback[$relationName];
}
if ($callback && is_array($callback) && isset($callback[$fullRelationName])) {
$relationCallback = $callback[$fullRelationName];
}
$alias = $joinHelper->getAliasName(
$useAlias,
$relation,
$relationName,
$relation->getQuery()->getModel()->getTable(),
$relationCallback
);
if ($alias && $relation instanceof BelongsToMany && !is_array($alias)) {
$extraAlias = $joinHelper->getAliasName(
$useAlias,
$relation,
$relationName,
$relation->getTable(),
$relationCallback
);
$alias = [$extraAlias, $alias];
}
$aliasString = is_array($alias) ? implode('.', $alias) : $alias;
$useAlias = $alias ? true : $useAlias;
if ($alias) {
$relationJoinCache = $latestRelation
? "{$aliasString}.{$relation->getQuery()->getModel()->getTable()}.{$latestRelation->getModel()->getTable()}.{$relationName}"
: "{$aliasString}.{$relation->getQuery()->getModel()->getTable()}.{$relationName}";
} else {
$relationJoinCache = $latestRelation
? "{$relation->getQuery()->getModel()->getTable()}.{$latestRelation->getModel()->getTable()}.{$relationName}"
: "{$relation->getQuery()->getModel()->getTable()}.{$relationName}";
}
if ($useAlias) {
StaticCache::setTableAliasForModel($relation->getModel(), $alias);
}
if ($joinHelper->relationshipAlreadyJoined($this->getModel(), $relationJoinCache)) {
$latestRelation = $relation;
continue;
}
$relation->performJoinForEloquentPowerJoins(
$this,
$joinType,
$relationCallback,
$alias,
$disableExtraConditions
);
$latestRelation = $relation;
$joinHelper->markRelationshipAsAlreadyJoined($this->getModel(), $relationJoinCache);
}
StaticCache::clear();
return $this;
};
}
/**
* Order by a field in the defined relationship.
*/
public function orderByPowerJoins(): Closure
{
return function ($sort, $direction = 'asc', $aggregation = null, $joinType = 'join') {
if (is_array($sort)) {
$relationships = explode('.', $sort[0]);
$column = $sort[1];
$latestRelationshipName = $relationships[count($relationships) - 1];
} else {
$relationships = explode('.', $sort);
$column = array_pop($relationships);
$latestRelationshipName = $relationships[count($relationships) - 1];
}
$this->joinRelationship(implode('.', $relationships), null, $joinType);
$latestRelationshipModel = array_reduce($relationships, function ($model, $relationshipName) {
return $model->$relationshipName()->getModel();
}, $this->getModel());
if ($aggregation) {
$aliasName = sprintf(
'%s_%s_%s',
$latestRelationshipModel->getTable(),
$column,
$aggregation
);
$this->selectRaw(
sprintf(
'%s(%s.%s) as %s',
$aggregation,
$latestRelationshipModel->getTable(),
$column,
$aliasName
)
)
->groupBy(sprintf('%s.%s', $this->getModel()->getTable(), $this->getModel()->getKeyName()))
->orderBy(DB::raw(sprintf('%s', $aliasName)), $direction);
} else {
if ($column instanceof Expression) {
$this->orderBy($column, $direction);
} else {
$this->orderBy(
sprintf('%s.%s', $latestRelationshipModel->getTable(), $column),
$direction
);
}
}
return $this;
};
}
public function orderByLeftPowerJoins(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, null, 'leftJoin');
};
}
/**
* Order by the COUNT aggregation using joins.
*/
public function orderByPowerJoinsCount(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'COUNT');
};
}
public function orderByLeftPowerJoinsCount(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'COUNT', 'leftJoin');
};
}
/**
* Order by the SUM aggregation using joins.
*/
public function orderByPowerJoinsSum(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'SUM');
};
}
public function orderByLeftPowerJoinsSum(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'SUM', 'leftJoin');
};
}
/**
* Order by the AVG aggregation using joins.
*/
public function orderByPowerJoinsAvg(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'AVG');
};
}
public function orderByLeftPowerJoinsAvg(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'AVG', 'leftJoin');
};
}
/**
* Order by the MIN aggregation using joins.
*/
public function orderByPowerJoinsMin(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'MIN');
};
}
public function orderByLeftPowerJoinsMin(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'MIN', 'leftJoin');
};
}
/**
* Order by the MAX aggregation using joins.
*/
public function orderByPowerJoinsMax(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'MAX');
};
}
public function orderByLeftPowerJoinsMax(): Closure
{
return function ($sort, $direction = 'asc') {
return $this->orderByPowerJoins($sort, $direction, 'MAX', 'leftJoin');
};
}
/**
* Same as Laravel 'has`, but using joins instead of where exists.
*/
public function powerJoinHas(): Closure
{
return function ($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure|array $callback = null, string $morphable = null): static {
if (is_null($this->getSelect())) {
$this->select(sprintf('%s.*', $this->getModel()->getTable()));
}
if (is_null($this->getGroupBy())) {
$this->groupBy($this->getModel()->getQualifiedKeyName());
}
if (is_string($relation)) {
if (Str::contains($relation, '.')) {
$this->hasNestedUsingJoins($relation, $operator, $count, 'and', $callback);
return $this;
}
$relation = $this->getRelationWithoutConstraintsProxy($relation);
}
$relation->performJoinForEloquentPowerJoins($this, 'leftPowerJoin', $callback, morphable: $morphable);
$relation->performHavingForEloquentPowerJoins($this, $operator, $count, morphable: $morphable);
return $this;
};
}
public function hasNestedUsingJoins(): Closure
{
return function ($relations, $operator = '>=', $count = 1, $boolean = 'and', Closure|array $callback = null): static {
$relations = explode('.', $relations);
/** @var Relation */
$latestRelation = null;
foreach ($relations as $index => $relation) {
$relationName = $relation;
if (!$latestRelation) {
$relation = $this->getRelationWithoutConstraintsProxy($relation);
} else {
$relation = $latestRelation->getModel()->query()->getRelationWithoutConstraintsProxy($relation);
}
$relation->performJoinForEloquentPowerJoins($this, 'leftPowerJoin', is_callable($callback) ? $callback : $callback[$relationName] ?? null);
if (count($relations) === ($index + 1)) {
$relation->performHavingForEloquentPowerJoins($this, $operator, $count);
}
$latestRelation = $relation;
}
return $this;
};
}
public function powerJoinDoesntHave(): Closure
{
return function ($relation, $boolean = 'and', Closure $callback = null) {
return $this->powerJoinHas($relation, '<', 1, $boolean, $callback);
};
}
public function powerJoinWhereHas(): Closure
{
return function ($relation, $callback = null, $operator = '>=', $count = 1) {
return $this->powerJoinHas($relation, $operator, $count, 'and', $callback);
};
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
class QueryBuilderExtraMethods
{
public function getGroupBy()
{
return function () {
return $this->groups;
};
}
public function getSelect()
{
return function () {
return $this->columns;
};
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
use Illuminate\Database\Eloquent\Relations\Relation;
class QueryRelationshipExistence
{
public function getGroupBy()
{
return function () {
return $this->getQuery()->getGroupBy();
};
}
public function getSelect()
{
return function () {
return $this->getQuery()->getSelect();
};
}
protected function getRelationWithoutConstraintsProxy()
{
return function ($relation) {
return Relation::noConstraints(function () use ($relation) {
return $this->getModel()->{$relation}();
});
};
}
}

View File

@@ -0,0 +1,538 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
use Stringable;
use Illuminate\Support\Str;
use Kirschbaum\PowerJoins\StaticCache;
use Kirschbaum\PowerJoins\PowerJoinClause;
use Kirschbaum\PowerJoins\Tests\Models\Post;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
/**
* @method \Illuminate\Database\Eloquent\Model getModel()
* @method string getTable()
* @method string getForeignPivotKeyName()
* @method string getRelatedPivotKeyName()
* @method bool isOneOfMany()
* @method \Illuminate\Database\Eloquent\Builder|void getOneOfManySubQuery()
* @method \Illuminate\Database\Eloquent\Builder getQuery()
* @method \Illuminate\Database\Eloquent\Model getThroughParent()
* @method string getForeignKeyName()
* @method string getMorphType()
* @method string getMorphClass()
* @method string getFirstKeyName()
* @method string getQualifiedLocalKeyName()
* @method string getExistenceCompareKey()
* @mixin \Illuminate\Database\Eloquent\Relations\Relation
* @mixin \Illuminate\Database\Eloquent\Relations\HasOneOrMany
* @mixin \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @property \Illuminate\Database\Eloquent\Builder $query
* @property \Illuminate\Database\Eloquent\Model $parent
* @property \Illuminate\Database\Eloquent\Model $throughParent
* @property string $foreignKey
* @property string $parentKey
* @property string $ownerKey
* @property string $localKey
* @property string $secondKey
* @property string $secondLocalKey
* @property \Illuminate\Database\Eloquent\Model $farParent
*/
class RelationshipsExtraMethods
{
/**
* Perform the JOIN clause for eloquent power joins.
*/
public function performJoinForEloquentPowerJoins()
{
return function ($builder, $joinType = 'leftJoin', $callback = null, $alias = null, bool $disableExtraConditions = false, string $morphable = null) {
return match (true) {
$this instanceof MorphToMany => $this->performJoinForEloquentPowerJoinsForMorphToMany($builder, $joinType, $callback, $alias, $disableExtraConditions),
$this instanceof BelongsToMany => $this->performJoinForEloquentPowerJoinsForBelongsToMany($builder, $joinType, $callback, $alias, $disableExtraConditions),
$this instanceof MorphOneOrMany => $this->performJoinForEloquentPowerJoinsForMorph($builder, $joinType, $callback, $alias, $disableExtraConditions),
$this instanceof HasMany || $this instanceof HasOne => $this->performJoinForEloquentPowerJoinsForHasMany($builder, $joinType, $callback, $alias, $disableExtraConditions),
$this instanceof HasManyThrough => $this->performJoinForEloquentPowerJoinsForHasManyThrough($builder, $joinType, $callback, $alias, $disableExtraConditions),
$this instanceof MorphTo => $this->performJoinForEloquentPowerJoinsForMorphTo($builder, $joinType, $callback, $alias, $disableExtraConditions, $morphable),
default => $this->performJoinForEloquentPowerJoinsForBelongsTo($builder, $joinType, $callback, $alias, $disableExtraConditions),
};
};
}
/**
* Perform the JOIN clause for the BelongsTo (or similar) relationships.
*/
protected function performJoinForEloquentPowerJoinsForBelongsTo()
{
return function ($query, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) {
$joinedTable = $this->query->getModel()->getTable();
$parentTable = StaticCache::getTableOrAliasForModel($this->parent);
$query->{$joinType}($joinedTable, function ($join) use ($callback, $joinedTable, $parentTable, $alias, $disableExtraConditions) {
if ($alias) {
$join->as($alias);
}
$join->on(
"{$parentTable}.{$this->foreignKey}",
'=',
"{$joinedTable}.{$this->ownerKey}"
);
if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getModel())) {
$join->whereNull("{$joinedTable}.{$this->query->getModel()->getDeletedAtColumn()}");
}
if ($disableExtraConditions === false) {
$this->applyExtraConditions($join);
}
if ($callback && is_callable($callback)) {
$callback($join);
}
}, $this->query->getModel());
};
}
/**
* Perform the JOIN clause for the BelongsToMany (or similar) relationships.
*/
protected function performJoinForEloquentPowerJoinsForBelongsToMany()
{
return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) {
[$alias1, $alias2] = $alias;
$joinedTable = $alias1 ?: $this->getTable();
$parentTable = StaticCache::getTableOrAliasForModel($this->parent);
$builder->{$joinType}($this->getTable(), function ($join) use ($callback, $joinedTable, $parentTable, $alias1) {
if ($alias1) {
$join->as($alias1);
}
$join->on(
"{$joinedTable}.{$this->getForeignPivotKeyName()}",
'=',
"{$parentTable}.{$this->parentKey}"
);
if (is_array($callback) && isset($callback[$this->getTable()])) {
$callback[$this->getTable()]($join);
}
});
$builder->{$joinType}($this->getModel()->getTable(), function ($join) use ($callback, $joinedTable, $alias2, $disableExtraConditions) {
if ($alias2) {
$join->as($alias2);
}
$join->on(
"{$this->getModel()->getTable()}.{$this->getModel()->getKeyName()}",
'=',
"{$joinedTable}.{$this->getRelatedPivotKeyName()}"
);
if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getModel())) {
$join->whereNull($this->query->getModel()->getQualifiedDeletedAtColumn());
}
// applying any extra conditions to the belongs to many relationship
if ($disableExtraConditions === false) {
$this->applyExtraConditions($join);
}
if (is_array($callback) && isset($callback[$this->getModel()->getTable()])) {
$callback[$this->getModel()->getTable()]($join);
}
}, $this->getModel());
return $this;
};
}
/**
* Perform the JOIN clause for the MorphToMany (or similar) relationships.
*/
protected function performJoinForEloquentPowerJoinsForMorphToMany()
{
return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) {
[$alias1, $alias2] = $alias;
$joinedTable = $alias1 ?: $this->getTable();
$parentTable = StaticCache::getTableOrAliasForModel($this->parent);
$builder->{$joinType}($this->getTable(), function ($join) use ($callback, $joinedTable, $parentTable, $alias1, $disableExtraConditions) {
if ($alias1) {
$join->as($alias1);
}
$join->on(
"{$joinedTable}.{$this->getForeignPivotKeyName()}",
'=',
"{$parentTable}.{$this->parentKey}"
);
// applying any extra conditions to the belongs to many relationship
if ($disableExtraConditions === false) {
$this->applyExtraConditions($join);
}
if (is_array($callback) && isset($callback[$this->getTable()])) {
$callback[$this->getTable()]($join);
}
});
$builder->{$joinType}($this->getModel()->getTable(), function ($join) use ($callback, $joinedTable, $alias2, $disableExtraConditions) {
if ($alias2) {
$join->as($alias2);
}
$join->on(
"{$this->getModel()->getTable()}.{$this->getModel()->getKeyName()}",
'=',
"{$joinedTable}.{$this->getRelatedPivotKeyName()}"
);
if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getModel())) {
$join->whereNull($this->query->getModel()->getQualifiedDeletedAtColumn());
}
if (is_array($callback) && isset($callback[$this->getModel()->getTable()])) {
$callback[$this->getModel()->getTable()]($join);
}
}, $this->getModel());
return $this;
};
}
/**
* Perform the JOIN clause for the Morph (or similar) relationships.
*/
protected function performJoinForEloquentPowerJoinsForMorph()
{
return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) {
$builder->{$joinType}($this->getModel()->getTable(), function ($join) use ($callback, $disableExtraConditions) {
$join->on(
"{$this->getModel()->getTable()}.{$this->getForeignKeyName()}",
'=',
"{$this->parent->getTable()}.{$this->localKey}"
)->where("{$this->getModel()->getTable()}.{$this->getMorphType()}", '=', $this->getMorphClass());
if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getModel())) {
$join->whereNull($this->query->getModel()->getQualifiedDeletedAtColumn());
}
if ($disableExtraConditions === false) {
$this->applyExtraConditions($join);
}
if ($callback && is_callable($callback)) {
$callback($join);
}
}, $this->getModel());
return $this;
};
}
/**
* Perform the JOIN clause for when calling the morphTo method from the morphable class.
*/
protected function performJoinForEloquentPowerJoinsForMorphTo()
{
return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false, string $morphable = null) {
$modelInstance = new $morphable;
$builder->{$joinType}($modelInstance->getTable(), function ($join) use ($modelInstance, $callback, $disableExtraConditions) {
$join->on(
"{$this->getModel()->getTable()}.{$this->getForeignKeyName()}",
'=',
"{$modelInstance->getTable()}.{$modelInstance->getKeyName()}"
)->where("{$this->getModel()->getTable()}.{$this->getMorphType()}", '=', $modelInstance->getMorphClass());
if ($disableExtraConditions === false && $this->usesSoftDeletes($modelInstance)) {
$join->whereNull($modelInstance->getQualifiedDeletedAtColumn());
}
if ($disableExtraConditions === false) {
$this->applyExtraConditions($join);
}
if ($callback && is_callable($callback)) {
$callback($join);
}
}, $modelInstance);
return $this;
};
}
/**
* Perform the JOIN clause for the HasMany (or similar) relationships.
*/
protected function performJoinForEloquentPowerJoinsForHasMany()
{
return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) {
$joinedTable = $alias ?: $this->query->getModel()->getTable();
$parentTable = StaticCache::getTableOrAliasForModel($this->parent);
$isOneOfMany = method_exists($this, 'isOneOfMany') ? $this->isOneOfMany() : false;
if ($isOneOfMany) {
foreach ($this->getOneOfManySubQuery()->getQuery()->columns as $column) {
$builder->addSelect($column);
}
$builder->take(1);
}
$builder->{$joinType}($this->query->getModel()->getTable(), function ($join) use ($callback, $joinedTable, $parentTable, $alias, $disableExtraConditions) {
if ($alias) {
$join->as($alias);
}
$join->on(
$this->foreignKey,
'=',
"{$parentTable}.{$this->localKey}"
);
if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getModel())) {
$join->whereNull(
"{$joinedTable}.{$this->query->getModel()->getDeletedAtColumn()}"
);
}
if ($disableExtraConditions === false) {
$this->applyExtraConditions($join);
}
if ($callback && is_callable($callback)) {
$callback($join);
}
}, $this->query->getModel());
};
}
/**
* Perform the JOIN clause for the HasManyThrough relationships.
*/
protected function performJoinForEloquentPowerJoinsForHasManyThrough()
{
return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) {
[$alias1, $alias2] = $alias;
$throughTable = $alias1 ?: $this->getThroughParent()->getTable();
$farTable = $alias2 ?: $this->getModel()->getTable();
$builder->{$joinType}($this->getThroughParent()->getTable(), function (PowerJoinClause $join) use ($callback, $throughTable, $alias1, $disableExtraConditions) {
if ($alias1) {
$join->as($alias1);
}
$join->on(
"{$throughTable}.{$this->getFirstKeyName()}",
'=',
$this->getQualifiedLocalKeyName()
);
if ($disableExtraConditions === false && $this->usesSoftDeletes($this->getThroughParent())) {
$join->whereNull($this->getThroughParent()->getQualifiedDeletedAtColumn());
}
if ($disableExtraConditions === false) {
$this->applyExtraConditions($join);
}
if (is_array($callback) && isset($callback[$this->getThroughParent()->getTable()])) {
$callback[$this->getThroughParent()->getTable()]($join);
}
if ($callback && is_callable($callback)) {
$callback($join);
}
}, $this->getThroughParent());
$builder->{$joinType}($this->getModel()->getTable(), function (PowerJoinClause $join) use ($callback, $throughTable, $farTable, $alias1, $alias2) {
if ($alias2) {
$join->as($alias2);
}
$join->on(
"{$farTable}.{$this->secondKey}",
'=',
"{$throughTable}.{$this->secondLocalKey}"
);
if ($this->usesSoftDeletes($this->getModel())) {
$join->whereNull("{$farTable}.{$this->getModel()->getDeletedAtColumn()}");
}
if (is_array($callback) && isset($callback[$this->getModel()->getTable()])) {
$callback[$this->getModel()->getTable()]($join);
}
}, $this->getModel());
return $this;
};
}
/**
* Perform the "HAVING" clause for eloquent power joins.
*/
public function performHavingForEloquentPowerJoins()
{
return function ($builder, $operator, $count, string $morphable = null) {
if ($morphable) {
$modelInstance = new $morphable;
$builder
->selectRaw(sprintf('count(%s) as %s_count', $modelInstance->getQualifiedKeyName(), Str::replace('.', '_', $modelInstance->getTable())))
->havingRaw(sprintf('count(%s) %s %d', $modelInstance->getQualifiedKeyName(), $operator, $count));
} else {
$builder
->selectRaw(sprintf('count(%s) as %s_count', $this->query->getModel()->getQualifiedKeyName(), Str::replace('.', '_', $this->query->getModel()->getTable())))
->havingRaw(sprintf('count(%s) %s %d', $this->query->getModel()->getQualifiedKeyName(), $operator, $count));
}
};
}
/**
* Checks if the relationship model uses soft deletes.
*/
public function usesSoftDeletes()
{
return function ($model) {
return in_array(SoftDeletes::class, class_uses_recursive($model));
};
}
/**
* Get the throughParent for the HasManyThrough relationship.
*/
public function getThroughParent()
{
return function () {
return $this->throughParent;
};
}
/**
* Get the farParent for the HasManyThrough relationship.
*/
public function getFarParent()
{
return function () {
return $this->farParent;
};
}
public function applyExtraConditions()
{
return function (PowerJoinClause $join) {
foreach ($this->getQuery()->getQuery()->wheres as $condition) {
if ($this->shouldNotApplyExtraCondition($condition)) {
continue;
}
if (!in_array($condition['type'], ['Basic', 'Null', 'NotNull', 'Nested'])) {
continue;
}
$method = "apply{$condition['type']}Condition";
$this->$method($join, $condition);
}
};
}
public function applyBasicCondition()
{
return function ($join, $condition) {
$join->where($condition['column'], $condition['operator'], $condition['value'], $condition['boolean']);
};
}
public function applyNullCondition()
{
return function ($join, $condition) {
$join->whereNull($condition['column'], $condition['boolean']);
};
}
public function applyNotNullCondition()
{
return function ($join, $condition) {
$join->whereNotNull($condition['column'], $condition['boolean']);
};
}
public function applyNestedCondition()
{
return function ($join, $condition) {
$join->where(function ($q) use ($condition) {
foreach ($condition['query']->wheres as $condition) {
$method = "apply{$condition['type']}Condition";
$this->$method($q, $condition);
}
});
};
}
public function shouldNotApplyExtraCondition()
{
return function ($condition) {
if (isset($condition['column']) && Str::endsWith($condition['column'], '.')) {
return true;
}
if (! $key = $this->getPowerJoinExistenceCompareKey()) {
return true;
}
if (isset($condition['query'])) {
return false;
}
if (is_array($key)) {
return in_array($condition['column'], $key);
}
return $condition['column'] === $key;
};
}
public function getPowerJoinExistenceCompareKey()
{
return function () {
if ($this instanceof MorphTo) {
return [$this->getMorphType(), $this->getForeignKeyName()];
}
if ($this instanceof BelongsTo) {
return $this->getQualifiedOwnerKeyName();
}
if ($this instanceof HasMany || $this instanceof HasOne) {
return $this->getExistenceCompareKey();
}
if ($this instanceof HasManyThrough) {
return $this->getQualifiedFirstKeyName();
}
if ($this instanceof BelongsToMany) {
return $this->getExistenceCompareKey();
}
if ($this instanceof MorphOneOrMany) {
return [$this->getQualifiedMorphType(), $this->getExistenceCompareKey()];
}
};
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Kirschbaum\PowerJoins;
use Closure;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class PowerJoinClause extends JoinClause
{
/**
* @var \Illuminate\Database\Eloquent\Model
*/
public $model;
/**
* Table name backup in case an alias is being used.
*
* @var string
*/
public $tableName;
/**
* Alias name.
*/
public ?string $alias = null;
/**
* Joined table alias name (mostly for belongs to many aliases).
*/
public ?string $joinedTableAlias = null;
/**
* Create a new join clause instance.
*/
public function __construct(Builder $parentQuery, $type, string $table, Model $model = null)
{
parent::__construct($parentQuery, $type, $table);
$this->model = $model;
$this->tableName = $table;
}
/**
* Add an alias to the table being joined.
*/
public function as(string $alias, ?string $joinedTableAlias = null): self
{
$this->alias = $alias;
$this->joinedTableAlias = $joinedTableAlias;
$this->table = sprintf('%s as %s', $this->table, $alias);
$this->useTableAliasInConditions();
if ($this->model) {
StaticCache::setTableAliasForModel($this->model, $alias);
}
return $this;
}
public function on($first, $operator = null, $second = null, $boolean = 'and'): self
{
parent::on($first, $operator, $second, $boolean);
$this->useTableAliasInConditions();
return $this;
}
public function getModel()
{
return $this->model;
}
/**
* Apply the global scopes to the joined query.
*/
public function withGlobalScopes(): self
{
if (! $this->model) {
return $this;
}
foreach ($this->model->getGlobalScopes() as $scope) {
if ($scope instanceof Closure) {
$scope->call($this, $this);
continue;
}
if ($scope instanceof SoftDeletingScope) {
continue;
}
(new $scope())->apply($this, $this->model);
}
return $this;
}
/**
* Apply the table alias in the existing join conditions.
*/
protected function useTableAliasInConditions(): self
{
if (! $this->alias || ! $this->model) {
return $this;
}
$this->wheres = collect($this->wheres)->filter(function ($where) {
return in_array($where['type'] ?? '', ['Column', 'Basic']);
})->map(function ($where) {
$key = $this->model->getKeyName();
$table = $this->tableName;
$replaceMethod = sprintf('useAliasInWhere%sType', ucfirst($where['type']));
return $this->{$replaceMethod}($where);
})->toArray();
return $this;
}
protected function useAliasInWhereColumnType(array $where): array
{
$key = $this->model->getKeyName();
$table = $this->tableName;
// if it was already replaced, skip
if (Str::startsWith($where['first'] . '.', $this->alias . '.') || Str::startsWith($where['second'] . '.', $this->alias . '.')) {
return $where;
}
if (Str::contains($where['first'], $table) && Str::contains($where['second'], $table)) {
// if joining the same table, only replace the correct table.key pair
$where['first'] = str_replace($table . '.' . $key, $this->alias . '.' . $key, $where['first']);
$where['second'] = str_replace($table . '.' . $key, $this->alias . '.' . $key, $where['second']);
} else {
$where['first'] = str_replace($table . '.', $this->alias . '.', $where['first']);
$where['second'] = str_replace($table . '.', $this->alias . '.', $where['second']);
}
return $where;
}
protected function useAliasInWhereBasicType(array $where): array
{
$table = $this->tableName;
if (Str::startsWith($where['column'] . '.', $this->alias . '.')) {
return $where;
}
if (Str::contains($where['column'], $table)) {
// if joining the same table, only replace the correct table.key pair
$where['column'] = str_replace($table . '.', $this->alias . '.', $where['column']);
} else {
$where['column'] = str_replace($table . '.', $this->alias . '.', $where['column']);
}
return $where;
}
public function whereNull($columns, $boolean = 'and', $not = false)
{
if ($this->alias && Str::contains($columns, $this->tableName)) {
$columns = str_replace("{$this->tableName}.", "{$this->alias}.", $columns);
}
return parent::whereNull($columns, $boolean, $not);
}
public function newQuery(): self
{
return new static($this->newParentQuery(), $this->type, $this->table, $this->model); // <-- The model param is needed
}
public function where($column, $operator = null, $value = null, $boolean = 'and'): self
{
if ($this->alias && is_string($column) && Str::contains($column, $this->tableName)) {
$column = str_replace("{$this->tableName}.", "{$this->alias}.", $column);
} elseif ($this->alias && ! is_callable($column)) {
$column = $this->alias . '.' . $column;
}
if (is_callable($column)) {
$query = new self($this, $this->type, $this->table, $this->model);
$column($query);
return $this->addNestedWhereQuery($query);
} else {
return parent::where($column, $operator, $value, $boolean);
}
}
/**
* Remove the soft delete condition in case the model implements soft deletes.
*/
public function withTrashed(): self
{
if (! $this->getModel() || ! in_array(SoftDeletes::class, class_uses_recursive($this->getModel()))) {
return $this;
}
$this->wheres = array_filter($this->wheres, function ($where) {
if ($where['type'] === 'Null' && Str::contains($where['column'], $this->getModel()->getDeletedAtColumn())) {
return false;
}
return true;
});
return $this;
}
/**
* Remove the soft delete condition in case the model implements soft deletes.
*/
public function onlyTrashed(): self
{
if (! $this->getModel() || ! in_array(SoftDeletes::class, class_uses_recursive($this->getModel()))) {
return $this;
}
$this->wheres = array_map(function ($where) {
if ($where['type'] === 'Null' && Str::contains($where['column'], $this->getModel()->getDeletedAtColumn())) {
$where['type'] = 'NotNull';
}
return $where;
}, $this->wheres);
return $this;
}
public function __call($name, $arguments)
{
$scope = 'scope' . ucfirst($name);
if (! $this->getModel()) {
return;
}
if (method_exists($this->getModel(), $scope)) {
return $this->getModel()->{$scope}($this, ...$arguments);
} else {
if (static::hasMacro($name)) {
return $this->macroCall($name, $arguments);
}
$eloquentBuilder = $this->getModel()->newEloquentBuilder($this);
if (method_exists($eloquentBuilder, $name)) {
return $eloquentBuilder->{$name}(...$arguments);
}
throw new InvalidArgumentException(sprintf('Method %s does not exist in PowerJoinClause class', $name));
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Kirschbaum\PowerJoins;
trait PowerJoins
{
//
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Support\ServiceProvider;
class PowerJoinsServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot()
{
}
/**
* Register the application services.
*/
public function register()
{
EloquentJoins::registerEloquentMacros();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Database\Eloquent\Model;
class StaticCache
{
/**
* Cache to not join the same relationship twice.
* @var array<int, string>
*/
public static array $powerJoinAliasesCache = [];
public static function getTableOrAliasForModel(Model $model): string
{
return static::$powerJoinAliasesCache[spl_object_id($model)] ?? $model->getTable();
}
public static function setTableAliasForModel(Model $model, $alias): void
{
static::$powerJoinAliasesCache[spl_object_id($model)] = $alias;
}
public static function clear(): void
{
static::$powerJoinAliasesCache = [];
}
}