Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Agent Guidelines for Eloquent Power Joins

## Commands
- **Test**: `composer test` or `vendor/bin/phpunit`
- **Single test**: `vendor/bin/phpunit tests/JoinRelationshipTest.php` or `vendor/bin/phpunit --filter test_method_name`
- **Test with coverage**: `composer test-coverage`
- **Lint**: `composer lint` or `vendor/bin/php-cs-fixer fix -vvv --show-progress=dots --config=.php-cs-fixer.php`

## Code Style
- **PHP Version**: 8.2+
- **Framework**: Laravel 11.42+/12.0+ package
- **Formatting**: Uses PHP-CS-Fixer with @Symfony rules + custom overrides
- **Imports**: Use global namespace imports for classes/constants/functions, ordered alphabetically
- **Arrays**: Short syntax `[]`, trailing commas in multiline
- **Quotes**: Single quotes preferred
- **Test methods**: snake_case naming with `@test` annotation
- **Namespaces**: `Kirschbaum\PowerJoins` for src, `Kirschbaum\PowerJoins\Tests` for tests
- **Type hints**: Use strict typing, compact nullable syntax `?Type`
- **PHPDoc**: Left-aligned, no empty returns, ordered tags
- **Variables**: No yoda conditions (`$var === 'value'` not `'value' === $var`)
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@
]
}
},
"minimum-stability": "dev"
"minimum-stability": "stable"
}
2 changes: 2 additions & 0 deletions src/JoinsHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ protected function __construct()

public static function make($model): static
{
static::$instances ??= new WeakMap();

return static::$instances[$model] ??= new self();
}

Expand Down
11 changes: 6 additions & 5 deletions src/Mixins/RelationshipsExtraMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,21 +298,22 @@ protected function performJoinForEloquentPowerJoinsForHasMany()
if ($isOneOfMany && !$hasCheck) {
$column = $this->getOneOfManySubQuery()->getQuery()->columns[0];
$fkColumn = $this->getOneOfManySubQuery()->getQuery()->columns[1];
$localKey = $this->localKey;

$builder->where(function ($query) use ($column, $joinType, $joinedModel, $builder, $fkColumn) {
$query->whereIn($joinedModel->getQualifiedKeyName(), function ($query) use ($column, $joinedModel, $builder, $fkColumn) {
$builder->where(function ($query) use ($column, $joinType, $joinedModel, $builder, $fkColumn, $parentTable, $localKey) {
$query->whereIn($joinedModel->getQualifiedKeyName(), function ($query) use ($column, $joinedModel, $builder, $fkColumn, $parentTable, $localKey) {
$columnValue = $column->getValue($builder->getGrammar());
$direction = Str::contains($columnValue, 'min(') ? 'asc' : 'desc';

$columnName = Str::of($columnValue)->after('(')->before(')')->__toString();
$columnName = Str::replace(['"', "'", '`'], '', $columnName);

if ($builder->getConnection() instanceof MySqlConnection) {
$query->select('*')->from(function ($query) use ($joinedModel, $columnName, $fkColumn, $direction, $builder) {
$query->select('*')->from(function ($query) use ($joinedModel, $columnName, $fkColumn, $direction, $parentTable, $localKey) {
$query
->select($joinedModel->getQualifiedKeyName())
->from($joinedModel->getTable())
->whereColumn($fkColumn, $builder->getModel()->getQualifiedKeyName())
->whereColumn($fkColumn, "{$parentTable}.{$localKey}")
->orderBy($columnName, $direction)
->take(1);
});
Expand All @@ -321,7 +322,7 @@ protected function performJoinForEloquentPowerJoinsForHasMany()
->select($joinedModel->getQualifiedKeyName())
->distinct($columnName)
->from($joinedModel->getTable())
->whereColumn($fkColumn, $builder->getModel()->getQualifiedKeyName())
->whereColumn($fkColumn, "{$parentTable}.{$localKey}")
->orderBy($columnName, $direction)
->take(1);
}
Expand Down
76 changes: 76 additions & 0 deletions tests/LatestOfManyJoinTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Kirschbaum\PowerJoins\Tests;

use Kirschbaum\PowerJoins\Tests\Models\Address;
use Kirschbaum\PowerJoins\Tests\Models\RequestedAddress;

class LatestOfManyJoinTest extends TestCase
{
/** @test */
public function test_left_join_relationship_with_latest_of_many_uses_correct_foreign_key()
{
// Create test data
$address = Address::create([
'kvh_code' => 'KVH123',
'name' => 'Test Address',
]);

RequestedAddress::create([
'kvh_code' => 'KVH123',
'requested_at' => now()->subDays(2),
'status' => 'pending',
]);

RequestedAddress::create([
'kvh_code' => 'KVH123',
'requested_at' => now()->subDay(),
'status' => 'approved',
]);

// This should generate a query that uses the correct foreign key relationship
$query = Address::query()
->leftJoinRelationship('latest_requested_address')
->toSql();

// The issue: the generated query incorrectly uses addresses.id instead of addresses.kvh_code
// in the subquery for latestOfMany
// Expected: all joins should use kvh_code
// Actual: subquery uses addresses.id instead of addresses.kvh_code

// This assertion will fail because the subquery incorrectly uses addresses.id
$this->assertStringNotContainsString('"requested_addresses"."kvh_code" = "addresses"."id"', $query);
}

/** @test */
public function test_left_join_relationship_with_latest_of_many_returns_correct_data()
{
// Create test data
$address = Address::create([
'kvh_code' => 'KVH456',
'name' => 'Test Address 2',
]);

$oldRequest = RequestedAddress::create([
'kvh_code' => 'KVH456',
'requested_at' => now()->subDays(2),
'status' => 'pending',
]);

$latestRequest = RequestedAddress::create([
'kvh_code' => 'KVH456',
'requested_at' => now()->subDay(),
'status' => 'approved',
]);

// Execute the query
$result = Address::query()
->leftJoinRelationship('latest_requested_address')
->where('addresses.kvh_code', 'KVH456')
->first();

// This should work correctly if the join uses the right foreign key
$this->assertNotNull($result);
$this->assertEquals('KVH456', $result->kvh_code);
}
}
37 changes: 37 additions & 0 deletions tests/Models/Address.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Kirschbaum\PowerJoins\Tests\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;

class Address extends Model
{
use SoftDeletes;

protected $table = 'addresses';

protected $fillable = [
'kvh_code',
'name',
];

public function requested_addresses(): HasMany
{
return $this->hasMany(RequestedAddress::class, 'kvh_code', 'kvh_code');
}

/**
* Get the latest requested address for this access address.
*
* @return HasOne<RequestedAddress, $this>
*/
public function latest_requested_address(): HasOne
{
return $this->requested_addresses()
->one()
->latestOfMany('requested_at');
}
}
26 changes: 26 additions & 0 deletions tests/Models/RequestedAddress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Kirschbaum\PowerJoins\Tests\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class RequestedAddress extends Model
{
protected $table = 'requested_addresses';

protected $fillable = [
'kvh_code',
'requested_at',
'status',
];

protected $casts = [
'requested_at' => 'datetime',
];

public function address(): BelongsTo
{
return $this->belongsTo(Address::class, 'kvh_code', 'kvh_code');
}
}
4 changes: 3 additions & 1 deletion tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public function setUp(): void

protected function getPackageProviders($app)
{
return [PowerJoinsServiceProvider::class];
return [
PowerJoinsServiceProvider::class,
];
}

public function assertQueryContains(string $expected, string $actual, string $message = '', ?int $times = null): void
Expand Down
18 changes: 18 additions & 0 deletions tests/database/migrations/2020_03_16_000000_create_tables.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ public function up()
$table->foreignId('tag_id');
$table->morphs('taggable');
});

Schema::create('addresses', function (Blueprint $table) {
$table->increments('id');
$table->string('kvh_code')->unique();
$table->string('name');
$table->timestamps();
$table->softDeletes();
});

Schema::create('requested_addresses', function (Blueprint $table) {
$table->increments('id');
$table->string('kvh_code');
$table->timestamp('requested_at');
$table->string('status')->default('pending');
$table->timestamps();

$table->index('kvh_code');
});
}

/**
Expand Down