Skip to content

Commit b3400b4

Browse files
authored
fix: adding search using lazy loading for belongs to (#649)
* fix: adding search using lazy loading for belongs to * Fix styling * Fix styling * fix: wip * fix: wip --------- Co-authored-by: binaryk <[email protected]>
1 parent 6904d56 commit b3400b4

20 files changed

+719
-37
lines changed

RELEASE.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
#### Custom Search Callbacks for BelongsTo Relations
60+
61+
BelongsTo fields now support custom search callbacks for complete control over search behavior:
62+
63+
```php
64+
BelongsTo::make('user')->searchable(function ($query, $request, $value, $field, $repository) {
65+
return $query->whereHas('user', function ($q) use ($value) {
66+
$q->where('name', 'ilike', "%{$value}%")
67+
->orWhere('email', 'ilike', "%{$value}%");
68+
});
69+
})
70+
```
71+
72+
The callback receives all necessary parameters with the query as the first parameter for maximum flexibility.
73+
74+
📖 **[Field Methods Documentation →](docs-v2/content/en/api/fields.md)**
75+
76+
### ⚠️ Breaking Changes
77+
78+
#### Default Search Behavior Change
79+
80+
Repositories no longer search by primary key (ID) by default when no searchable fields are defined.
81+
82+
**Migration Path:**
83+
```php
84+
public static function searchables(): array {
85+
return empty(static::$search) ? [static::newModel()->getKeyName()] : static::$search;
86+
}
87+
```
88+
89+
📖 **[Complete Migration Guide →](UPGRADING.md)**
90+
91+
### 🔧 Technical Improvements
92+
93+
- **Scout Integration**: Enhanced error handling and graceful degradation
94+
- **Column Qualification**: Improved handling for JOIN operations
95+
- **SearchablesCollection**: Fixed string callable handling
96+
- **Configuration**: New options with environment variable support
97+
98+
## 📚 Documentation & Resources
99+
100+
- **[Complete Documentation](docs-v2/content/en/)** - Comprehensive guides and examples
101+
- **[Migration Guide](UPGRADING.md)** - Step-by-step upgrade instructions
102+
- **[MCP Integration](docs-v2/content/en/mcp/mcp.md)** - AI agent setup and configuration
103+
- **[Field Reference](docs-v2/content/en/api/fields.md)** - All field methods and options
104+
105+
## 🧪 Testing
106+
107+
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

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,29 @@ $isSearchable = $field->isSearchable(); // true
344344
$attributes = $field->getSearchables(); // ['name']
345345
```
346346

347+
#### Custom Search Callbacks
348+
349+
For advanced search scenarios, you can provide a custom callback to completely control the search behavior:
350+
351+
```php
352+
BelongsTo::make('user')->searchable(function ($query, $request, $value, $field, $repository) {
353+
return $query->whereHas('user', function ($q) use ($value) {
354+
$q->where('name', 'ilike', "%{$value}%")
355+
->orWhere('email', 'ilike', "%{$value}%")
356+
->orWhere('phone', 'like', "%{$value}%");
357+
});
358+
})
359+
```
360+
361+
The callback receives the following parameters:
362+
- `$query` - The main query builder instance
363+
- `$request` - The current RestifyRequest instance
364+
- `$value` - The search value from the request
365+
- `$field` - The BelongsTo field instance
366+
- `$repository` - The current repository instance
367+
368+
This approach provides maximum flexibility for complex search requirements while maintaining the same API interface.
369+
347370
## HasOne
348371

349372
The `HasOne` field corresponds to a `hasOne` Eloquent relationship.

0 commit comments

Comments
 (0)