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
107 changes: 107 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Release Notes

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.

## Version 10.x

### 🚀 Major Features

#### Model Context Protocol (MCP) Integration

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!

**Quick Setup:**
```php
use Binaryk\LaravelRestify\MCP\RestifyServer;
use Laravel\Mcp\Facades\Mcp;

// Web-based MCP server with authentication
Mcp::web('restify', RestifyServer::class)
->middleware(['auth:sanctum'])
->name('mcp.restify');
```

**Key Benefits:** AI-Ready APIs, Zero Configuration, Built-in Security, Web & Terminal Access

📖 **[Complete MCP Documentation →](docs-v2/content/en/mcp/mcp.md)**

#### Lazy Relationship Loading for Fields

Fields can now be configured to lazy load relationships, preventing N+1 queries for computed attributes:

```php
field('profileTagNames', fn() => $this->model()->profileTagNames)
->lazy('tags'),
```

📖 **[Lazy Loading Documentation →](docs-v2/content/en/api/fields.md#lazy-loading)**

#### JOIN Optimization for BelongsTo Search

Performance optimization replacing slow subqueries with efficient JOIN operations. Enable via configuration:

```php
// config/restify.php
'search' => [
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', false),
],
```

📖 **[Performance Optimization Guide →](UPGRADING.md#join-optimization)**

#### Enhanced Field Methods

New and improved field methods with flexible signatures:
- **`searchable()`** - Unified flexible signature with multiple argument support
- **`matchable()`** - Various match types and advanced filtering scenarios
- **`sortable()`** - Custom columns and conditional sorting

#### Custom Search Callbacks for BelongsTo Relations

BelongsTo fields now support custom search callbacks for complete control over search behavior:

```php
BelongsTo::make('user')->searchable(function ($query, $request, $value, $field, $repository) {
return $query->whereHas('user', function ($q) use ($value) {
$q->where('name', 'ilike', "%{$value}%")
->orWhere('email', 'ilike', "%{$value}%");
});
})
```

The callback receives all necessary parameters with the query as the first parameter for maximum flexibility.

📖 **[Field Methods Documentation →](docs-v2/content/en/api/fields.md)**

### ⚠️ Breaking Changes

#### Default Search Behavior Change

Repositories no longer search by primary key (ID) by default when no searchable fields are defined.

**Migration Path:**
```php
public static function searchables(): array {
return empty(static::$search) ? [static::newModel()->getKeyName()] : static::$search;
}
```

📖 **[Complete Migration Guide →](UPGRADING.md)**

### 🔧 Technical Improvements

- **Scout Integration**: Enhanced error handling and graceful degradation
- **Column Qualification**: Improved handling for JOIN operations
- **SearchablesCollection**: Fixed string callable handling
- **Configuration**: New options with environment variable support

## 📚 Documentation & Resources

- **[Complete Documentation](docs-v2/content/en/)** - Comprehensive guides and examples
- **[Migration Guide](UPGRADING.md)** - Step-by-step upgrade instructions
- **[MCP Integration](docs-v2/content/en/mcp/mcp.md)** - AI agent setup and configuration
- **[Field Reference](docs-v2/content/en/api/fields.md)** - All field methods and options

## 🧪 Testing

All new features include comprehensive test coverage to ensure reliability and maintainability.
93 changes: 93 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,92 @@ class UserRepository extends Repository

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

#### Enhanced BelongsTo Search Performance with Configurable JOINs

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.

**Performance Impact:**

**Before (v9 and earlier - Subquery approach):**
```sql
-- Slow subquery-based search
SELECT * FROM users WHERE (
(SELECT name FROM organizations WHERE organizations.id = users.organization_id LIMIT 1) LIKE '%Tech%'
OR
(SELECT phone FROM organizations WHERE organizations.id = users.organization_id LIMIT 1) LIKE '%Tech%'
)
```

**After (v10 - Optimized JOIN approach):**
```sql
-- Fast JOIN-based search with proper column selection
SELECT users.* FROM users
LEFT JOIN organizations ON users.organization_id = organizations.id
WHERE (organizations.name LIKE '%Tech%' OR organizations.phone LIKE '%Tech%')
```

**Configuration Options:**

The JOIN optimization can be controlled via configuration:

```php
// config/restify.php
'search' => [
'case_sensitive' => true,

/*
| Use JOINs for BelongsTo Relationships
| When enabled, BelongsTo relationship searches will use JOINs instead of
| subqueries for better performance. This is generally recommended for
| better query performance, but can be disabled if compatibility issues arise.
| Default: true (recommended for better performance)
*/
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', true),
],
```

**Environment Variable Control:**
```bash
# .env file
RESTIFY_USE_JOINS_FOR_BELONGS_TO=true # Enable JOINs (default, recommended)
RESTIFY_USE_JOINS_FOR_BELONGS_TO=false # Disable JOINs (legacy subqueries)
```

**Benefits of JOIN optimization:**
- 🚀 **Better Performance** - JOINs are significantly faster than subqueries for relationship searches
- 📊 **Improved Scalability** - Better performance with large datasets
- 🔧 **Automatic Column Qualification** - Prevents column name conflicts in complex queries
- ⚡ **Pagination Optimization** - Both main and count queries benefit from JOINs

**When to disable JOINs:**
- 🔄 **During migration** - Test both approaches during deployment
- 🐛 **Compatibility issues** - If you encounter any edge cases with complex queries
- 📊 **Specific database setups** - Some database configurations may prefer subqueries
- 🧪 **Testing phases** - Compare performance in your specific environment

**Migration Strategy:**

1. **Default behavior** - JOINs are enabled by default for better performance
2. **No code changes needed** - Existing BelongsTo searches automatically benefit
3. **Easy rollback** - Set `RESTIFY_USE_JOINS_FOR_BELONGS_TO=false` to revert to v9 behavior
4. **Gradual testing** - Test in development/staging before production deployment

**Example Usage:**
```php
// This automatically benefits from JOIN optimization in v10
class PostRepository extends Repository
{
public static array $related = [
'user' => BelongsTo::make('user', UserRepository::class)
->searchable(['name', 'email']),
'organization' => BelongsTo::make('organization', OrganizationRepository::class)
->searchable(['name', 'phone']),
];
}
```

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.

## Breaking Changes

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

```php
// Example new sections (check the actual config file for current options)
'search' => [
'case_sensitive' => true,

// New: JOIN optimization for BelongsTo searches (v10+)
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', true),
],

'mcp' => [
'tools' => [
'exclude' => [],
Expand Down
14 changes: 14 additions & 0 deletions config/restify.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@
| Specify either the search should be case-sensitive or not.
*/
'case_sensitive' => true,

/*
|--------------------------------------------------------------------------
| Use JOINs for BelongsTo Relationships
|--------------------------------------------------------------------------
|
| When enabled, BelongsTo relationship searches will use JOINs instead of
| subqueries for better performance. This is generally recommended for
| better query performance, but can be disabled if compatibility issues arise.
|
| Default: true (recommended for better performance)
|
*/
'use_joins_for_belongs_to' => env('RESTIFY_USE_JOINS_FOR_BELONGS_TO', false),
],

'repositories' => [
Expand Down
73 changes: 73 additions & 0 deletions docs-v2/content/en/api/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,79 @@ class AvatarStore implements Storable
You can use the <code>php artisan restify:store AvatarStore</code> command to generate a store file.
</alert>

## Lazy Loading

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.

### Making Fields Lazy

Use the `lazy()` method to mark a field for lazy loading:

```php
public function fields(RestifyRequest $request)
{
return [
// Lazy load the 'tags' relationship when displaying profileTagNames
field('profileTagNames', fn() => $this->model()->profileTagNames)
->lazy('tags'),

// Lazy load using the field's attribute name (if it matches the relationship)
field('tags', fn() => $this->model()->tags->pluck('name'))
->lazy(),

// Another example with user relationship
field('authorName', fn() => $this->model()->user->name ?? 'Unknown')
->lazy('user'),
];
}
```

### How It Works

When you have a model attribute like this:

```php
class Post extends Model
{
public function getProfileTagNamesAttribute(): array
{
return $this->tags()->pluck('name')->toArray();
}

public function tags()
{
return $this->belongsToMany(Tag::class);
}
}
```

You can create a field that efficiently loads this data:

```php
field('profileTagNames', fn() => $this->model()->profileTagNames)
->lazy('tags')
```

This ensures that:
1. The `tags` relationship is loaded before the field value is computed
2. Multiple fields using the same relationship won't cause additional queries
3. The computed value can safely access the relationship data

### Lazy Loading Methods

The `CanLoadLazyRelationship` trait provides the following methods:

- `lazy(?string $relationshipName = null)` - Mark the field as lazy and optionally specify the relationship name
- `isLazy(RestifyRequest $request)` - Check if the field is configured for lazy loading
- `getLazyRelationshipName()` - Get the name of the relationship to lazy load

### Benefits

- **Performance**: Prevents N+1 queries when dealing with computed attributes
- **Efficiency**: Relationships are loaded only once, even if multiple fields depend on them
- **Flexibility**: Works with any relationship type (BelongsTo, HasMany, ManyToMany, etc.)
- **Clean Code**: Keeps your field definitions simple while ensuring optimal database usage

## Utility Methods

### Repository Management
Expand Down
23 changes: 23 additions & 0 deletions docs-v2/content/en/api/relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,29 @@ $isSearchable = $field->isSearchable(); // true
$attributes = $field->getSearchables(); // ['name']
```

#### Custom Search Callbacks

For advanced search scenarios, you can provide a custom callback to completely control the search behavior:

```php
BelongsTo::make('user')->searchable(function ($query, $request, $value, $field, $repository) {
return $query->whereHas('user', function ($q) use ($value) {
$q->where('name', 'ilike', "%{$value}%")
->orWhere('email', 'ilike', "%{$value}%")
->orWhere('phone', 'like', "%{$value}%");
});
})
```

The callback receives the following parameters:
- `$query` - The main query builder instance
- `$request` - The current RestifyRequest instance
- `$value` - The search value from the request
- `$field` - The BelongsTo field instance
- `$repository` - The current repository instance

This approach provides maximum flexibility for complex search requirements while maintaining the same API interface.

## HasOne

The `HasOne` field corresponds to a `hasOne` Eloquent relationship.
Expand Down
Loading
Loading