Skip to content

Commit 562c65b

Browse files
committed
fix: adding search using lazy loading for belongs to
1 parent 632d3c2 commit 562c65b

21 files changed

+684
-89
lines changed

RELEASE.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Release Notes
2+
3+
This document provides a high-level overview of major features and changes in Laravel Restify. For detailed documentation and implementation guides, please refer to the comprehensive documentation.
4+
5+
## Version 10.x
6+
7+
### 🚀 Major Features
8+
9+
#### Model Context Protocol (MCP) Integration
10+
11+
Laravel Restify now provides seamless integration with the Model Context Protocol (MCP), allowing AI agents to interact with your REST API resources through structured tool interfaces. Transform your repositories into tools for AI agents to consume!
12+
13+
**Quick Setup:**
14+
```php
15+
use Binaryk\LaravelRestify\MCP\RestifyServer;
16+
use Laravel\Mcp\Facades\Mcp;
17+
18+
// Web-based MCP server with authentication
19+
Mcp::web('restify', RestifyServer::class)
20+
->middleware(['auth:sanctum'])
21+
->name('mcp.restify');
22+
```
23+
24+
**Key Benefits:** AI-Ready APIs, Zero Configuration, Built-in Security, Web & Terminal Access
25+
26+
📖 **[Complete MCP Documentation →](docs-v2/content/en/mcp/mcp.md)**
27+
28+
#### Lazy Relationship Loading for Fields
29+
30+
Fields can now be configured to lazy load relationships, preventing N+1 queries for computed attributes:
31+
32+
```php
33+
field('profileTagNames', fn() => $this->model()->profileTagNames)
34+
->lazy('tags'),
35+
```
36+
37+
📖 **[Lazy Loading Documentation →](docs-v2/content/en/api/fields.md#lazy-loading)**
38+
39+
#### JOIN Optimization for BelongsTo Search
40+
41+
Performance optimization replacing slow subqueries with efficient JOIN operations. Enable via configuration:
42+
43+
```php
44+
// config/restify.php
45+
'search' => [
46+
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', false),
47+
],
48+
```
49+
50+
📖 **[Performance Optimization Guide →](UPGRADING.md#join-optimization)**
51+
52+
#### Enhanced Field Methods
53+
54+
New and improved field methods with flexible signatures:
55+
- **`searchable()`** - Unified flexible signature with multiple argument support
56+
- **`matchable()`** - Various match types and advanced filtering scenarios
57+
- **`sortable()`** - Custom columns and conditional sorting
58+
59+
📖 **[Field Methods Documentation →](docs-v2/content/en/api/fields.md)**
60+
61+
### ⚠️ Breaking Changes
62+
63+
#### Default Search Behavior Change
64+
65+
Repositories no longer search by primary key (ID) by default when no searchable fields are defined.
66+
67+
**Migration Path:**
68+
```php
69+
public static function searchables(): array {
70+
return empty(static::$search) ? [static::newModel()->getKeyName()] : static::$search;
71+
}
72+
```
73+
74+
📖 **[Complete Migration Guide →](UPGRADING.md)**
75+
76+
### 🔧 Technical Improvements
77+
78+
- **Scout Integration**: Enhanced error handling and graceful degradation
79+
- **Column Qualification**: Improved handling for JOIN operations
80+
- **SearchablesCollection**: Fixed string callable handling
81+
- **Configuration**: New options with environment variable support
82+
83+
## 📚 Documentation & Resources
84+
85+
- **[Complete Documentation](docs-v2/content/en/)** - Comprehensive guides and examples
86+
- **[Migration Guide](UPGRADING.md)** - Step-by-step upgrade instructions
87+
- **[MCP Integration](docs-v2/content/en/mcp/mcp.md)** - AI agent setup and configuration
88+
- **[Field Reference](docs-v2/content/en/api/fields.md)** - All field methods and options
89+
90+
## 🧪 Testing
91+
92+
All new features include comprehensive test coverage to ensure reliability and maintainability.

UPGRADING.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,92 @@ class UserRepository extends Repository
119119

120120
This change is also **100% backward compatible** - existing static arrays continue to work perfectly.
121121

122+
#### Enhanced BelongsTo Search Performance with Configurable JOINs
123+
124+
Laravel Restify v10 introduces a significant performance optimization for BelongsTo relationship searches by replacing slow subqueries with efficient JOINs. This feature is configurable and enabled by default for better performance.
125+
126+
**Performance Impact:**
127+
128+
**Before (v9 and earlier - Subquery approach):**
129+
```sql
130+
-- Slow subquery-based search
131+
SELECT * FROM users WHERE (
132+
(SELECT name FROM organizations WHERE organizations.id = users.organization_id LIMIT 1) LIKE '%Tech%'
133+
OR
134+
(SELECT phone FROM organizations WHERE organizations.id = users.organization_id LIMIT 1) LIKE '%Tech%'
135+
)
136+
```
137+
138+
**After (v10 - Optimized JOIN approach):**
139+
```sql
140+
-- Fast JOIN-based search with proper column selection
141+
SELECT users.* FROM users
142+
LEFT JOIN organizations ON users.organization_id = organizations.id
143+
WHERE (organizations.name LIKE '%Tech%' OR organizations.phone LIKE '%Tech%')
144+
```
145+
146+
**Configuration Options:**
147+
148+
The JOIN optimization can be controlled via configuration:
149+
150+
```php
151+
// config/restify.php
152+
'search' => [
153+
'case_sensitive' => true,
154+
155+
/*
156+
| Use JOINs for BelongsTo Relationships
157+
| When enabled, BelongsTo relationship searches will use JOINs instead of
158+
| subqueries for better performance. This is generally recommended for
159+
| better query performance, but can be disabled if compatibility issues arise.
160+
| Default: true (recommended for better performance)
161+
*/
162+
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', true),
163+
],
164+
```
165+
166+
**Environment Variable Control:**
167+
```bash
168+
# .env file
169+
RESTIFY_USE_JOINS_FOR_BELONGS_TO=true # Enable JOINs (default, recommended)
170+
RESTIFY_USE_JOINS_FOR_BELONGS_TO=false # Disable JOINs (legacy subqueries)
171+
```
172+
173+
**Benefits of JOIN optimization:**
174+
- 🚀 **Better Performance** - JOINs are significantly faster than subqueries for relationship searches
175+
- 📊 **Improved Scalability** - Better performance with large datasets
176+
- 🔧 **Automatic Column Qualification** - Prevents column name conflicts in complex queries
177+
-**Pagination Optimization** - Both main and count queries benefit from JOINs
178+
179+
**When to disable JOINs:**
180+
- 🔄 **During migration** - Test both approaches during deployment
181+
- 🐛 **Compatibility issues** - If you encounter any edge cases with complex queries
182+
- 📊 **Specific database setups** - Some database configurations may prefer subqueries
183+
- 🧪 **Testing phases** - Compare performance in your specific environment
184+
185+
**Migration Strategy:**
186+
187+
1. **Default behavior** - JOINs are enabled by default for better performance
188+
2. **No code changes needed** - Existing BelongsTo searches automatically benefit
189+
3. **Easy rollback** - Set `RESTIFY_USE_JOINS_FOR_BELONGS_TO=false` to revert to v9 behavior
190+
4. **Gradual testing** - Test in development/staging before production deployment
191+
192+
**Example Usage:**
193+
```php
194+
// This automatically benefits from JOIN optimization in v10
195+
class PostRepository extends Repository
196+
{
197+
public static array $related = [
198+
'user' => BelongsTo::make('user', UserRepository::class)
199+
->searchable(['name', 'email']),
200+
'organization' => BelongsTo::make('organization', OrganizationRepository::class)
201+
->searchable(['name', 'phone']),
202+
];
203+
}
204+
```
205+
206+
This change is **100% backward compatible** with an option to disable if needed. The optimization is transparent to your application code while providing significant performance improvements.
207+
122208
## Breaking Changes
123209

124210
### Default Search Behavior Change
@@ -179,6 +265,13 @@ When upgrading to v10, it's important to ensure your local `config/restify.php`
179265

180266
```php
181267
// Example new sections (check the actual config file for current options)
268+
'search' => [
269+
'case_sensitive' => true,
270+
271+
// New: JOIN optimization for BelongsTo searches (v10+)
272+
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', true),
273+
],
274+
182275
'mcp' => [
183276
'tools' => [
184277
'exclude' => [],

config/restify.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@
132132
| Specify either the search should be case-sensitive or not.
133133
*/
134134
'case_sensitive' => true,
135+
136+
/*
137+
|--------------------------------------------------------------------------
138+
| Use JOINs for BelongsTo Relationships
139+
|--------------------------------------------------------------------------
140+
|
141+
| When enabled, BelongsTo relationship searches will use JOINs instead of
142+
| subqueries for better performance. This is generally recommended for
143+
| better query performance, but can be disabled if compatibility issues arise.
144+
|
145+
| Default: true (recommended for better performance)
146+
|
147+
*/
148+
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', false),
135149
],
136150

137151
'repositories' => [

docs-v2/content/en/api/fields.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,79 @@ class AvatarStore implements Storable
13741374
You can use the <code>php artisan restify:store AvatarStore</code> command to generate a store file.
13751375
</alert>
13761376

1377+
## Lazy Loading
1378+
1379+
Fields can be configured to lazy load relationships, which is particularly useful for computed attributes that depend on related models. This helps avoid N+1 queries by ensuring relationships are loaded only when needed.
1380+
1381+
### Making Fields Lazy
1382+
1383+
Use the `lazy()` method to mark a field for lazy loading:
1384+
1385+
```php
1386+
public function fields(RestifyRequest $request)
1387+
{
1388+
return [
1389+
// Lazy load the 'tags' relationship when displaying profileTagNames
1390+
field('profileTagNames', fn() => $this->model()->profileTagNames)
1391+
->lazy('tags'),
1392+
1393+
// Lazy load using the field's attribute name (if it matches the relationship)
1394+
field('tags', fn() => $this->model()->tags->pluck('name'))
1395+
->lazy(),
1396+
1397+
// Another example with user relationship
1398+
field('authorName', fn() => $this->model()->user->name ?? 'Unknown')
1399+
->lazy('user'),
1400+
];
1401+
}
1402+
```
1403+
1404+
### How It Works
1405+
1406+
When you have a model attribute like this:
1407+
1408+
```php
1409+
class Post extends Model
1410+
{
1411+
public function getProfileTagNamesAttribute(): array
1412+
{
1413+
return $this->tags()->pluck('name')->toArray();
1414+
}
1415+
1416+
public function tags()
1417+
{
1418+
return $this->belongsToMany(Tag::class);
1419+
}
1420+
}
1421+
```
1422+
1423+
You can create a field that efficiently loads this data:
1424+
1425+
```php
1426+
field('profileTagNames', fn() => $this->model()->profileTagNames)
1427+
->lazy('tags')
1428+
```
1429+
1430+
This ensures that:
1431+
1. The `tags` relationship is loaded before the field value is computed
1432+
2. Multiple fields using the same relationship won't cause additional queries
1433+
3. The computed value can safely access the relationship data
1434+
1435+
### Lazy Loading Methods
1436+
1437+
The `CanLoadLazyRelationship` trait provides the following methods:
1438+
1439+
- `lazy(?string $relationshipName = null)` - Mark the field as lazy and optionally specify the relationship name
1440+
- `isLazy(RestifyRequest $request)` - Check if the field is configured for lazy loading
1441+
- `getLazyRelationshipName()` - Get the name of the relationship to lazy load
1442+
1443+
### Benefits
1444+
1445+
- **Performance**: Prevents N+1 queries when dealing with computed attributes
1446+
- **Efficiency**: Relationships are loaded only once, even if multiple fields depend on them
1447+
- **Flexibility**: Works with any relationship type (BelongsTo, HasMany, ManyToMany, etc.)
1448+
- **Clean Code**: Keeps your field definitions simple while ensuring optimal database usage
1449+
13771450
## Utility Methods
13781451

13791452
### Repository Management
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\Fields\Concerns;
4+
5+
use Binaryk\LaravelRestify\Fields\Field;
6+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
7+
8+
/**
9+
* @mixin Field
10+
*/
11+
trait CanLoadLazyRelationship
12+
{
13+
protected ?string $lazyRelationshipName = null;
14+
15+
public function lazy(string $relationshipName = null): self
16+
{
17+
$this->lazyRelationshipName = $relationshipName ?? $this->getAttribute();
18+
19+
return $this;
20+
}
21+
22+
public function isLazy(RestifyRequest $request): bool
23+
{
24+
return !is_null($this->lazyRelationshipName);
25+
}
26+
27+
public function getLazyRelationshipName(): ?string
28+
{
29+
return $this->lazyRelationshipName;
30+
}
31+
}

src/Fields/EagerField.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function __construct($attribute, ?string $parentRepository = null)
4545

4646
if (is_null($parentRepository)) {
4747
$this->repositoryClass = tap(Restify::repositoryClassForKey(str($attribute)->pluralStudly()->kebab()->toString()),
48-
fn ($repository) => abort_unless($repository, 400, "Repository not found for the key [$attribute]."));
48+
fn($repository) => abort_unless($repository, 400, "Repository not found for the key [$attribute]."));
4949
}
5050

5151
if (! isset($this->repositoryClass)) {
@@ -61,9 +61,9 @@ public function __construct($attribute, ?string $parentRepository = null)
6161
public function authorize(Request $request)
6262
{
6363
return call_user_func(
64-
[$this->repositoryClass, 'authorizedToUseRepository'],
65-
$request
66-
) && parent::authorize($request);
64+
[$this->repositoryClass, 'authorizedToUseRepository'],
65+
$request
66+
) && parent::authorize($request);
6767
}
6868

6969
public function resolve($repository, $attribute = null)

src/Fields/Field.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Binaryk\LaravelRestify\Fields;
44

5+
use Binaryk\LaravelRestify\Fields\Concerns\CanLoadLazyRelationship;
56
use Binaryk\LaravelRestify\Fields\Concerns\CanMatch;
67
use Binaryk\LaravelRestify\Fields\Concerns\CanSearch;
78
use Binaryk\LaravelRestify\Fields\Concerns\CanSort;
@@ -26,6 +27,7 @@ class Field extends OrganicField implements JsonSerializable, Matchable, Sortabl
2627
use CanMatch;
2728
use CanSearch;
2829
use CanSort;
30+
use CanLoadLazyRelationship;
2931
use FieldMcpSchemaDetection;
3032
use HasAction;
3133
use Make;

src/Fields/HasMany.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
77
use Binaryk\LaravelRestify\Repositories\Repository;
88
use Illuminate\Auth\Access\AuthorizationException;
9+
use Illuminate\Database\Eloquent\Relations\Relation;
910
use Illuminate\Http\Request;
1011

1112
class HasMany extends EagerField
@@ -28,6 +29,9 @@ public function resolve($repository, $attribute = null)
2829
if ($repository->model()->relationLoaded($this->relation)) {
2930
$paginator = $repository->model()->getRelation($this->relation);
3031
} else {
32+
/**
33+
* @var Relation $paginator
34+
*/
3135
$paginator = $repository->{$this->relation}();
3236
$paginator = $paginator
3337
->take(request('relatablePerPage') ?? ($this->repositoryClass::$defaultRelatablePerPage ?? RestifySearchable::DEFAULT_RELATABLE_PER_PAGE))

0 commit comments

Comments
 (0)