Optimizations

This commit is contained in:
myrmidex 2025-08-16 10:47:50 +02:00
parent 2087ca389e
commit 58848c934e
11 changed files with 1143 additions and 51 deletions

View file

@ -3,19 +3,23 @@
namespace App\Http\Controllers\Api\V1;
use Domains\Settings\Models\Language;
use Domains\Settings\Services\LanguageService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class LanguagesController extends BaseController
{
public function __construct(
private LanguageService $languageService
) {}
/**
* Get languages available for route creation
* Returns languages that have both active feeds and active channels
*/
public function availableForRoutes(): JsonResponse
{
$languages = Language::availableForRoutes()
->orderBy('name')
->get(['id', 'short_code', 'name', 'native_name']);
$languages = $this->languageService->getAvailableForRoutes();
return $this->sendResponse(
$languages,
@ -26,15 +30,20 @@ public function availableForRoutes(): JsonResponse
/**
* Get feeds filtered by language
*/
public function feedsByLanguage(int $languageId): JsonResponse
public function feedsByLanguage(Request $request, int $languageId): JsonResponse
{
$language = Language::findOrFail($languageId);
$fields = $this->parseFields($request->get('fields'), [
'feeds.id', 'feeds.name', 'feeds.url', 'feeds.type', 'feeds.provider'
]);
$feeds = $language->feeds()
->where('feeds.is_active', true)
->where('feed_languages.is_active', true)
->with('languages')
->get(['feeds.id', 'feeds.name', 'feeds.url', 'feeds.type', 'feeds.provider']);
$feeds = $this->languageService->getFeedsByLanguage(
$languageId,
$request->get('search'),
(int) $request->get('per_page', 15),
$fields
);
$language = Language::select(['name'])->findOrFail($languageId);
return $this->sendResponse(
$feeds,
@ -45,18 +54,93 @@ public function feedsByLanguage(int $languageId): JsonResponse
/**
* Get channels filtered by language
*/
public function channelsByLanguage(int $languageId): JsonResponse
public function channelsByLanguage(Request $request, int $languageId): JsonResponse
{
$language = Language::findOrFail($languageId);
$fields = $this->parseFields($request->get('fields'), [
'id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id'
]);
$channels = $language->platformChannels()
->where('platform_channels.is_active', true)
->with(['platformInstance:id,name,url', 'language:id,name,short_code'])
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id']);
$channels = $this->languageService->getChannelsByLanguage(
$languageId,
$request->get('search'),
(int) $request->get('per_page', 15),
$fields
);
$language = Language::select(['name'])->findOrFail($languageId);
return $this->sendResponse(
$channels,
"Channels for language '{$language->name}' retrieved successfully."
);
}
/**
* Get language statistics
*/
public function statistics(int $languageId): JsonResponse
{
$stats = $this->languageService->getLanguageStatistics($languageId);
$language = Language::select(['name'])->findOrFail($languageId);
return $this->sendResponse(
$stats,
"Statistics for language '{$language->name}' retrieved successfully."
);
}
/**
* Get usage summary for all languages
*/
public function usageSummary(): JsonResponse
{
$summary = $this->languageService->getLanguageUsageSummary();
return $this->sendResponse(
$summary,
'Language usage summary retrieved successfully.'
);
}
/**
* Get common languages between feed and channel
*/
public function commonLanguages(Request $request): JsonResponse
{
$request->validate([
'feed_id' => 'required|exists:feeds,id',
'channel_id' => 'required|exists:platform_channels,id'
]);
$commonLanguages = $this->languageService->getCommonLanguages(
$request->get('feed_id'),
$request->get('channel_id')
);
return $this->sendResponse(
$commonLanguages,
'Common languages retrieved successfully.'
);
}
/**
* Parse field selection from request
*/
private function parseFields(?string $fieldsParam, array $defaultFields): array
{
if (!$fieldsParam) {
return $defaultFields;
}
$requestedFields = array_map('trim', explode(',', $fieldsParam));
$validFields = [];
foreach ($requestedFields as $field) {
if (in_array($field, $defaultFields) || str_contains($field, '.')) {
$validFields[] = $field;
}
}
return empty($validFields) ? $defaultFields : $validFields;
}
}

View file

@ -8,18 +8,22 @@
use Domains\Feed\Models\Route;
use Domains\Feed\Requests\StoreRouteRequest;
use Domains\Feed\Requests\UpdateRouteRequest;
use Domains\Settings\Services\LanguageService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class RoutingController extends BaseController
{
public function __construct(
private LanguageService $languageService
) {}
/**
* Display a listing of routing configurations
*/
public function index(): JsonResponse
{
$routes = Route::with(['feed', 'platformChannel', 'language', 'keywords'])
$routes = Route::withAllRelationships()
->orderBy('is_active', 'desc')
->orderBy('priority', 'asc')
->get();
@ -43,8 +47,17 @@ public function store(StoreRouteRequest $request): JsonResponse
$route = Route::create($validated);
// Load relationships efficiently
$route->load([
'feed:id,name,url,type,provider,is_active',
'platformChannel:id,platform_instance_id,name,display_name,description,language_id,is_active',
'platformChannel.platformInstance:id,name,url',
'language:id,short_code,name,native_name,is_active',
'keywords:id,feed_id,platform_channel_id,keyword,is_active'
]);
return $this->sendResponse(
new RouteResource($route->load(['feed', 'platformChannel', 'language', 'keywords'])),
new RouteResource($route),
'Routing configuration created successfully!',
201
);
@ -66,7 +79,13 @@ public function show(Feed $feed, PlatformChannel $channel): JsonResponse
return $this->sendNotFound('Routing configuration not found.');
}
$route->load(['feed', 'platformChannel', 'language', 'keywords']);
$route->load([
'feed:id,name,url,type,provider,is_active',
'platformChannel:id,platform_instance_id,name,display_name,description,language_id,is_active',
'platformChannel.platformInstance:id,name,url',
'language:id,short_code,name,native_name,is_active',
'keywords:id,feed_id,platform_channel_id,keyword,is_active'
]);
return $this->sendResponse(
new RouteResource($route),
@ -156,6 +175,19 @@ public function toggle(Feed $feed, PlatformChannel $channel): JsonResponse
}
}
/**
* Get common languages for feed and channel
*/
public function commonLanguages(Feed $feed, PlatformChannel $channel): JsonResponse
{
$commonLanguages = $this->languageService->getCommonLanguages($feed->id, $channel->id);
return $this->sendResponse(
$commonLanguages,
'Common languages for feed and channel retrieved successfully.'
);
}
/**
* Find a route by feed and channel
*/

View file

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Add composite index on feed_languages for language filtering queries
Schema::table('feed_languages', function (Blueprint $table) {
$table->index(['language_id', 'is_active', 'is_primary'], 'idx_feed_languages_filtering');
});
// Add index on platform_channels for language-based channel lookups
Schema::table('platform_channels', function (Blueprint $table) {
$table->index(['language_id', 'is_active'], 'idx_platform_channels_language_active');
});
// Add composite index on routes for efficient language-based queries
Schema::table('routes', function (Blueprint $table) {
$table->index(['language_id', 'is_active', 'priority'], 'idx_routes_language_active_priority');
});
// Add index for language consistency validation queries
Schema::table('feed_languages', function (Blueprint $table) {
$table->index(['feed_id', 'language_id', 'is_active'], 'idx_feed_languages_validation');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('feed_languages', function (Blueprint $table) {
$table->dropIndex('idx_feed_languages_filtering');
$table->dropIndex('idx_feed_languages_validation');
});
Schema::table('platform_channels', function (Blueprint $table) {
$table->dropIndex('idx_platform_channels_language_active');
});
Schema::table('routes', function (Blueprint $table) {
$table->dropIndex('idx_routes_language_active_priority');
});
}
};

View file

@ -10,6 +10,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
/**
@ -129,4 +130,42 @@ public function getCommonLanguages(): array
return in_array($channelLanguageId, $feedLanguageIds) ? [$channelLanguageId] : [];
}
/**
* Scope for eager loading all route relationships
*/
public function scopeWithAllRelationships(Builder $query): Builder
{
return $query->with([
'feed:id,name,url,type,provider,is_active',
'platformChannel:id,platform_instance_id,name,display_name,description,language_id,is_active',
'platformChannel.platformInstance:id,name,url',
'language:id,short_code,name,native_name,is_active',
'keywords:id,feed_id,platform_channel_id,keyword,is_active'
]);
}
/**
* Scope for minimal route data
*/
public function scopeMinimal(Builder $query): Builder
{
return $query->select(['feed_id', 'platform_channel_id', 'language_id', 'is_active', 'priority']);
}
/**
* Scope for active routes only
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
/**
* Scope for routes by language
*/
public function scopeByLanguage(Builder $query, int $languageId): Builder
{
return $query->where('language_id', $languageId);
}
}

View file

@ -3,6 +3,7 @@
namespace Domains\Feed\Requests;
use Domains\Feed\Models\Feed;
use Domains\Feed\Services\LanguageConsistencyValidator;
use Domains\Platform\Models\PlatformChannel;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
@ -56,18 +57,12 @@ protected function validateLanguageConsistency(Validator $validator): void
return;
}
// Check if feed has the selected language
$feed = Feed::find($feedId);
if (!$feed || !$feed->languages()->where('languages.id', $languageId)->exists()) {
$validator->errors()->add('language_id', 'The selected feed does not support this language.');
return;
}
$consistencyValidator = app(LanguageConsistencyValidator::class);
$result = $consistencyValidator->validateConsistency($feedId, $channelId, $languageId);
// Check if channel has the selected language
$channel = PlatformChannel::find($channelId);
if (!$channel || $channel->language_id !== (int)$languageId) {
$validator->errors()->add('language_id', 'The selected channel does not support this language.');
return;
if (!$result['is_valid']) {
$message = $consistencyValidator->getValidationMessage($result);
$validator->errors()->add('language_id', $message);
}
}

View file

@ -3,6 +3,7 @@
namespace Domains\Feed\Requests;
use Domains\Feed\Models\Feed;
use Domains\Feed\Services\LanguageConsistencyValidator;
use Domains\Platform\Models\PlatformChannel;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
@ -61,16 +62,12 @@ protected function validateLanguageConsistency(Validator $validator): void
return;
}
// Check if feed has the selected language
if (!$feed->languages()->where('languages.id', $languageId)->exists()) {
$validator->errors()->add('language_id', 'The selected feed does not support this language.');
return;
}
$consistencyValidator = app(LanguageConsistencyValidator::class);
$result = $consistencyValidator->validateConsistency($feed->id, $channel->id, $languageId);
// Check if channel has the selected language
if ($channel->language_id !== (int)$languageId) {
$validator->errors()->add('language_id', 'The selected channel does not support this language.');
return;
if (!$result['is_valid']) {
$message = $consistencyValidator->getValidationMessage($result);
$validator->errors()->add('language_id', $message);
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace Domains\Feed\Services;
use Domains\Feed\Models\Feed;
use Domains\Platform\Models\PlatformChannel;
use Illuminate\Support\Facades\DB;
class LanguageConsistencyValidator
{
/**
* Validate that a language is consistent between feed and channel in a single optimized query
*/
public function validateConsistency(int $feedId, int $channelId, int $languageId): array
{
// Single query to check both feed and channel language consistency
$result = DB::selectOne("
SELECT
EXISTS(
SELECT 1
FROM feed_languages fl
JOIN feeds f ON f.id = fl.feed_id
WHERE fl.feed_id = ?
AND fl.language_id = ?
AND fl.is_active = 1
AND f.is_active = 1
) as feed_has_language,
EXISTS(
SELECT 1
FROM platform_channels pc
WHERE pc.id = ?
AND pc.language_id = ?
AND pc.is_active = 1
) as channel_has_language
", [$feedId, $languageId, $channelId, $languageId]);
return [
'is_valid' => $result->feed_has_language && $result->channel_has_language,
'feed_supports_language' => (bool) $result->feed_has_language,
'channel_supports_language' => (bool) $result->channel_has_language,
];
}
/**
* Get validation error message based on the validation result
*/
public function getValidationMessage(array $validationResult): ?string
{
if ($validationResult['is_valid']) {
return null;
}
if (!$validationResult['feed_supports_language']) {
return 'The selected feed does not support this language.';
}
if (!$validationResult['channel_supports_language']) {
return 'The selected channel does not support this language.';
}
return 'Language consistency validation failed.';
}
/**
* Batch validate multiple feed+channel+language combinations
*/
public function batchValidate(array $combinations): array
{
if (empty($combinations)) {
return [];
}
$conditions = [];
$params = [];
foreach ($combinations as $index => $combo) {
$feedId = $combo['feed_id'];
$channelId = $combo['platform_channel_id'];
$languageId = $combo['language_id'];
$conditions[] = "(fl.feed_id = ? AND fl.language_id = ? AND pc.id = ? AND pc.language_id = ?)";
$params = array_merge($params, [$feedId, $languageId, $channelId, $languageId]);
}
$sql = "
SELECT
fl.feed_id,
pc.id as channel_id,
fl.language_id,
COUNT(*) as is_valid
FROM feed_languages fl
JOIN feeds f ON f.id = fl.feed_id AND f.is_active = 1
JOIN platform_channels pc ON pc.language_id = fl.language_id AND pc.is_active = 1
WHERE fl.is_active = 1
AND (" . implode(' OR ', $conditions) . ")
GROUP BY fl.feed_id, pc.id, fl.language_id
";
$results = DB::select($sql, $params);
// Convert results to associative array for easy lookup
$validCombinations = [];
foreach ($results as $result) {
$key = "{$result->feed_id}_{$result->channel_id}_{$result->language_id}";
$validCombinations[$key] = $result->is_valid > 0;
}
// Map back to original combinations
$output = [];
foreach ($combinations as $index => $combo) {
$key = "{$combo['feed_id']}_{$combo['platform_channel_id']}_{$combo['language_id']}";
$output[$index] = [
'is_valid' => $validCombinations[$key] ?? false,
'feed_id' => $combo['feed_id'],
'channel_id' => $combo['platform_channel_id'],
'language_id' => $combo['language_id'],
];
}
return $output;
}
/**
* Get common languages between a feed and channel efficiently
*/
public function getCommonLanguages(int $feedId, int $channelId): array
{
$results = DB::select("
SELECT DISTINCT fl.language_id, l.short_code, l.name
FROM feed_languages fl
JOIN feeds f ON f.id = fl.feed_id AND f.is_active = 1
JOIN platform_channels pc ON pc.language_id = fl.language_id AND pc.is_active = 1
JOIN languages l ON l.id = fl.language_id AND l.is_active = 1
WHERE fl.feed_id = ?
AND pc.id = ?
AND fl.is_active = 1
ORDER BY l.name
", [$feedId, $channelId]);
return array_map(function ($result) {
return [
'id' => $result->language_id,
'short_code' => $result->short_code,
'name' => $result->name,
];
}, $results);
}
}

View file

@ -12,6 +12,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
class Language extends Model
{
@ -78,13 +79,20 @@ public function routes(): HasMany
*/
public function scopeAvailableForRoutes(Builder $query): Builder
{
return $query->where('is_active', true)
->whereHas('feeds', function (Builder $feedQuery) {
$feedQuery->where('feeds.is_active', true)
return $query->where('languages.is_active', true)
->whereExists(function ($subQuery) {
$subQuery->select(DB::raw(1))
->from('feed_languages')
->join('feeds', 'feeds.id', '=', 'feed_languages.feed_id')
->whereColumn('feed_languages.language_id', 'languages.id')
->where('feeds.is_active', true)
->where('feed_languages.is_active', true);
})
->whereHas('platformChannels', function (Builder $channelQuery) {
$channelQuery->where('platform_channels.is_active', true);
->whereExists(function ($subQuery) {
$subQuery->select(DB::raw(1))
->from('platform_channels')
->whereColumn('platform_channels.language_id', 'languages.id')
->where('platform_channels.is_active', true);
});
}
@ -96,8 +104,12 @@ public function scopeAvailableForRoutes(Builder $query): Builder
*/
public function scopeWithActiveFeeds(Builder $query): Builder
{
return $query->whereHas('feeds', function (Builder $feedQuery) {
$feedQuery->where('feeds.is_active', true)
return $query->whereExists(function ($subQuery) {
$subQuery->select(\DB::raw(1))
->from('feed_languages')
->join('feeds', 'feeds.id', '=', 'feed_languages.feed_id')
->whereColumn('feed_languages.language_id', 'languages.id')
->where('feeds.is_active', true)
->where('feed_languages.is_active', true);
});
}
@ -110,8 +122,11 @@ public function scopeWithActiveFeeds(Builder $query): Builder
*/
public function scopeWithActiveChannels(Builder $query): Builder
{
return $query->whereHas('platformChannels', function (Builder $channelQuery) {
$channelQuery->where('platform_channels.is_active', true);
return $query->whereExists(function ($subQuery) {
$subQuery->select(\DB::raw(1))
->from('platform_channels')
->whereColumn('platform_channels.language_id', 'languages.id')
->where('platform_channels.is_active', true);
});
}
@ -120,8 +135,75 @@ public function scopeWithActiveChannels(Builder $query): Builder
*/
public function canBeUsedForRoutes(): bool
{
return $this->is_active &&
$this->feeds()->where('feeds.is_active', true)->where('feed_languages.is_active', true)->exists() &&
$this->platformChannels()->where('platform_channels.is_active', true)->exists();
if (!$this->is_active) {
return false;
}
// Use a single query to check both conditions efficiently
$hasActiveFeeds = DB::table('feed_languages')
->join('feeds', 'feeds.id', '=', 'feed_languages.feed_id')
->where('feed_languages.language_id', $this->id)
->where('feeds.is_active', true)
->where('feed_languages.is_active', true)
->exists();
if (!$hasActiveFeeds) {
return false;
}
return DB::table('platform_channels')
->where('language_id', $this->id)
->where('is_active', true)
->exists();
}
/**
* Scope for eager loading language data with feeds and channels
*/
public function scopeWithLanguageData(Builder $query): Builder
{
return $query->with([
'feeds' => function ($query) {
$query->where('feeds.is_active', true)
->where('feed_languages.is_active', true)
->select(['feeds.id', 'feeds.name', 'feeds.url', 'feeds.type', 'feeds.provider']);
},
'platformChannels' => function ($query) {
$query->where('platform_channels.is_active', true)
->with(['platformInstance:id,name,url'])
->select(['id', 'platform_instance_id', 'name', 'display_name', 'description', 'language_id']);
},
'routes' => function ($query) {
$query->where('routes.is_active', true)
->select(['id', 'feed_id', 'platform_channel_id', 'language_id', 'is_active', 'priority']);
}
]);
}
/**
* Scope for minimal language data (for dropdowns and lists)
*/
public function scopeMinimal(Builder $query): Builder
{
return $query->select(['id', 'short_code', 'name', 'native_name', 'is_active']);
}
/**
* Scope for language data with counts
*/
public function scopeWithCounts(Builder $query): Builder
{
return $query->withCount([
'feeds as active_feeds_count' => function ($query) {
$query->where('feeds.is_active', true)
->where('feed_languages.is_active', true);
},
'platformChannels as active_channels_count' => function ($query) {
$query->where('platform_channels.is_active', true);
},
'routes as active_routes_count' => function ($query) {
$query->where('routes.is_active', true);
}
]);
}
}

View file

@ -0,0 +1,289 @@
<?php
namespace Domains\Settings\Repositories;
use Domains\Settings\Models\Language;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Pagination\LengthAwarePaginator;
class LanguageRepository
{
public function __construct(
private Language $model
) {}
/**
* Find languages available for route creation with optimized query
*/
public function findAvailableForRoutes(array $select = ['*']): Collection
{
return $this->model
->select($select)
->where('languages.is_active', true)
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('feed_languages')
->join('feeds', 'feeds.id', '=', 'feed_languages.feed_id')
->whereColumn('feed_languages.language_id', 'languages.id')
->where('feeds.is_active', true)
->where('feed_languages.is_active', true);
})
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('platform_channels')
->whereColumn('platform_channels.language_id', 'languages.id')
->where('platform_channels.is_active', true);
})
->orderBy('name')
->get();
}
/**
* Find feeds by language with efficient joins
*/
public function findFeedsByLanguage(
int $languageId,
?string $search = null,
array $select = ['feeds.*'],
int $perPage = 15
): LengthAwarePaginator {
$query = DB::table('feeds')
->join('feed_languages', 'feeds.id', '=', 'feed_languages.feed_id')
->where('feed_languages.language_id', $languageId)
->where('feeds.is_active', true)
->where('feed_languages.is_active', true)
->select($select);
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('feeds.name', 'like', "%{$search}%")
->orWhere('feeds.url', 'like', "%{$search}%");
});
}
return $query->orderBy('feeds.name')
->paginate(min($perPage, 100));
}
/**
* Find channels by language with efficient joins
*/
public function findChannelsByLanguage(
int $languageId,
?string $search = null,
array $select = ['platform_channels.*'],
int $perPage = 15,
bool $withPlatformInstance = true
): LengthAwarePaginator {
$query = DB::table('platform_channels');
if ($withPlatformInstance) {
$query->join('platform_instances', 'platform_channels.platform_instance_id', '=', 'platform_instances.id')
->addSelect([
'platform_instances.id as platform_instance_id',
'platform_instances.name as platform_instance_name',
'platform_instances.url as platform_instance_url'
]);
}
$query->where('platform_channels.language_id', $languageId)
->where('platform_channels.is_active', true)
->select(array_merge($select, $withPlatformInstance ? [] : []));
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('platform_channels.name', 'like', "%{$search}%")
->orWhere('platform_channels.display_name', 'like', "%{$search}%");
});
}
return $query->orderBy('platform_channels.name')
->paginate(min($perPage, 100));
}
/**
* Get language statistics with single optimized query
*/
public function getLanguageStatistics(int $languageId): object
{
return DB::selectOne("
SELECT
(SELECT COUNT(*) FROM feed_languages fl
JOIN feeds f ON f.id = fl.feed_id
WHERE fl.language_id = ? AND fl.is_active = 1 AND f.is_active = 1) as active_feeds_count,
(SELECT COUNT(*) FROM platform_channels pc
WHERE pc.language_id = ? AND pc.is_active = 1) as active_channels_count,
(SELECT COUNT(*) FROM routes r
WHERE r.language_id = ? AND r.is_active = 1) as active_routes_count
", [$languageId, $languageId, $languageId]);
}
/**
* Get comprehensive language usage summary
*/
public function getLanguageUsageSummary(): array
{
$results = DB::select("
SELECT
l.id,
l.short_code,
l.name,
l.native_name,
l.is_active,
COALESCE(feed_stats.active_feeds, 0) as active_feeds_count,
COALESCE(channel_stats.active_channels, 0) as active_channels_count,
COALESCE(route_stats.active_routes, 0) as active_routes_count,
CASE WHEN COALESCE(feed_stats.active_feeds, 0) > 0
AND COALESCE(channel_stats.active_channels, 0) > 0
THEN 1 ELSE 0 END as can_create_routes
FROM languages l
LEFT JOIN (
SELECT fl.language_id, COUNT(*) as active_feeds
FROM feed_languages fl
INNER JOIN feeds f ON f.id = fl.feed_id
WHERE fl.is_active = 1 AND f.is_active = 1
GROUP BY fl.language_id
) feed_stats ON feed_stats.language_id = l.id
LEFT JOIN (
SELECT pc.language_id, COUNT(*) as active_channels
FROM platform_channels pc
WHERE pc.is_active = 1
GROUP BY pc.language_id
) channel_stats ON channel_stats.language_id = l.id
LEFT JOIN (
SELECT r.language_id, COUNT(*) as active_routes
FROM routes r
WHERE r.is_active = 1
GROUP BY r.language_id
) route_stats ON route_stats.language_id = l.id
ORDER BY l.name
");
return array_map(fn($row) => (array)$row, $results);
}
/**
* Find common languages between feed and channel with optimized query
*/
public function findCommonLanguages(int $feedId, int $channelId): array
{
$results = DB::select("
SELECT DISTINCT l.id, l.short_code, l.name, l.native_name
FROM languages l
INNER JOIN feed_languages fl ON fl.language_id = l.id
INNER JOIN feeds f ON f.id = fl.feed_id
INNER JOIN platform_channels pc ON pc.language_id = l.id
WHERE f.id = ?
AND pc.id = ?
AND l.is_active = 1
AND f.is_active = 1
AND fl.is_active = 1
AND pc.is_active = 1
ORDER BY l.name
", [$feedId, $channelId]);
return array_map(fn($row) => (array)$row, $results);
}
/**
* Check if language can be used for routes with optimized query
*/
public function canLanguageBeUsedForRoutes(int $languageId): bool
{
$result = DB::selectOne("
SELECT
EXISTS(SELECT 1 FROM feed_languages fl
JOIN feeds f ON f.id = fl.feed_id
WHERE fl.language_id = ? AND fl.is_active = 1 AND f.is_active = 1) as has_feeds,
EXISTS(SELECT 1 FROM platform_channels pc
WHERE pc.language_id = ? AND pc.is_active = 1) as has_channels
", [$languageId, $languageId]);
return $result->has_feeds && $result->has_channels;
}
/**
* Get active languages with optional counts
*/
public function findActiveLanguages(
bool $withCounts = false,
array $select = ['*']
): Collection {
$query = $this->model->where('is_active', true);
if ($withCounts) {
$query->withCounts();
}
if ($select !== ['*'] && !empty($select)) {
$query->select($select);
}
return $query->orderBy('name')->get();
}
/**
* Batch check language availability for multiple language IDs
*/
public function batchCheckLanguageAvailability(array $languageIds): array
{
if (empty($languageIds)) {
return [];
}
$placeholders = str_repeat('?,', count($languageIds) - 1) . '?';
$results = DB::select("
SELECT
l.id,
EXISTS(SELECT 1 FROM feed_languages fl
JOIN feeds f ON f.id = fl.feed_id
WHERE fl.language_id = l.id AND fl.is_active = 1 AND f.is_active = 1) as has_feeds,
EXISTS(SELECT 1 FROM platform_channels pc
WHERE pc.language_id = l.id AND pc.is_active = 1) as has_channels
FROM languages l
WHERE l.id IN ({$placeholders}) AND l.is_active = 1
", $languageIds);
$availability = [];
foreach ($results as $result) {
$availability[$result->id] = $result->has_feeds && $result->has_channels;
}
return $availability;
}
/**
* Get language with relationship counts for specific language
*/
public function findWithCounts(int $languageId): ?Language
{
return $this->model
->withCounts()
->find($languageId);
}
/**
* Search languages by name or code
*/
public function search(
string $term,
bool $activeOnly = true,
array $select = ['*']
): Collection {
$query = $this->model->select($select)
->where(function ($q) use ($term) {
$q->where('name', 'like', "%{$term}%")
->orWhere('native_name', 'like', "%{$term}%")
->orWhere('short_code', 'like', "%{$term}%");
});
if ($activeOnly) {
$query->where('is_active', true);
}
return $query->orderBy('name')->get();
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace Domains\Settings\Services;
use Domains\Settings\Models\Language;
use Domains\Settings\Repositories\LanguageRepository;
use Domains\Feed\Services\LanguageConsistencyValidator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class LanguageService
{
public function __construct(
private LanguageRepository $languageRepository,
private LanguageConsistencyValidator $consistencyValidator
) {}
/**
* Get languages available for route creation
*/
public function getAvailableForRoutes(array $fields = []): Collection
{
$select = !empty($fields) ? $fields : ['id', 'short_code', 'name', 'native_name', 'is_active'];
return $this->languageRepository->findAvailableForRoutes($select);
}
/**
* Get feeds by language with search and pagination
*/
public function getFeedsByLanguage(
int $languageId,
?string $search = null,
int $perPage = 15,
array $fields = []
): LengthAwarePaginator {
$defaultFields = [
'feeds.id', 'feeds.name', 'feeds.url', 'feeds.type', 'feeds.provider'
];
$selectedFields = !empty($fields) ? $fields : $defaultFields;
return $this->languageRepository->findFeedsByLanguage(
$languageId,
$search,
$selectedFields,
$perPage
);
}
/**
* Get channels by language with search and pagination
*/
public function getChannelsByLanguage(
int $languageId,
?string $search = null,
int $perPage = 15,
array $fields = []
): LengthAwarePaginator {
$defaultFields = [
'platform_channels.id', 'platform_channels.platform_instance_id',
'platform_channels.name', 'platform_channels.display_name',
'platform_channels.description', 'platform_channels.language_id'
];
$selectedFields = !empty($fields) ? $fields : $defaultFields;
return $this->languageRepository->findChannelsByLanguage(
$languageId,
$search,
$selectedFields,
$perPage,
true // withPlatformInstance
);
}
/**
* Get language statistics
*/
public function getLanguageStatistics(int $languageId): object
{
return $this->languageRepository->getLanguageStatistics($languageId);
}
/**
* Validate route language consistency
*/
public function validateRouteLanguage(int $feedId, int $channelId, int $languageId): array
{
return $this->consistencyValidator->validateConsistency($feedId, $channelId, $languageId);
}
/**
* Batch validate multiple route combinations
*/
public function batchValidateRoutes(array $routeData): array
{
return $this->consistencyValidator->batchValidate($routeData);
}
/**
* Get common languages between a feed and channel
*/
public function getCommonLanguages(int $feedId, int $channelId): array
{
return $this->languageRepository->findCommonLanguages($feedId, $channelId);
}
/**
* Check if a language can be used for routes
*/
public function canLanguageBeUsedForRoutes(int $languageId): bool
{
return $this->languageRepository->canLanguageBeUsedForRoutes($languageId);
}
/**
* Get language usage summary
*/
public function getLanguageUsageSummary(): array
{
return $this->languageRepository->getLanguageUsageSummary();
}
/**
* Get languages with detailed relationship counts
*/
public function getLanguagesWithCounts(array $fields = []): Collection
{
return $this->languageRepository->findActiveLanguages(true, $fields);
}
/**
* Find optimal language for feed-channel pair
*/
public function findOptimalLanguage(int $feedId, int $channelId): ?array
{
$commonLanguages = $this->getCommonLanguages($feedId, $channelId);
if (empty($commonLanguages)) {
return null;
}
// Return the first available language (could be enhanced with priority logic)
return $commonLanguages[0];
}
}

View file

@ -0,0 +1,226 @@
<?php
namespace Tests\Unit\Services;
use Domains\Settings\Services\LanguageService;
use Domains\Settings\Models\Language;
use Domains\Feed\Models\Feed;
use Domains\Platform\Models\PlatformChannel;
use Domains\Platform\Models\PlatformInstance;
use Domains\Feed\Services\LanguageConsistencyValidator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LanguageServiceTest extends TestCase
{
use RefreshDatabase;
private LanguageService $languageService;
private Language $language;
private Feed $feed;
private PlatformChannel $channel;
protected function setUp(): void
{
parent::setUp();
$this->languageService = app(LanguageService::class);
// Create test data
$this->language = Language::factory()->create(['is_active' => true]);
$this->feed = Feed::factory()->create(['is_active' => true]);
$platformInstance = PlatformInstance::factory()->create();
$this->channel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id,
'language_id' => $this->language->id,
'is_active' => true
]);
// Create feed-language relationship
$this->feed->languages()->attach($this->language->id, [
'url' => $this->feed->url,
'is_active' => true,
'is_primary' => true
]);
}
public function test_get_available_for_routes_returns_languages_with_feeds_and_channels(): void
{
$languages = $this->languageService->getAvailableForRoutes();
$this->assertCount(1, $languages);
$this->assertEquals($this->language->id, $languages->first()->id);
}
public function test_get_feeds_by_language_returns_paginated_results(): void
{
$result = $this->languageService->getFeedsByLanguage($this->language->id);
$this->assertInstanceOf(\Illuminate\Pagination\LengthAwarePaginator::class, $result);
$this->assertCount(1, $result->items());
$this->assertEquals($this->feed->id, $result->items()[0]->id);
}
public function test_get_feeds_by_language_with_search(): void
{
$result = $this->languageService->getFeedsByLanguage(
$this->language->id,
$this->feed->name
);
$this->assertCount(1, $result->items());
// Test search that shouldn't match
$result = $this->languageService->getFeedsByLanguage(
$this->language->id,
'nonexistent'
);
$this->assertCount(0, $result->items());
}
public function test_get_channels_by_language_returns_paginated_results(): void
{
$result = $this->languageService->getChannelsByLanguage($this->language->id);
$this->assertInstanceOf(\Illuminate\Pagination\LengthAwarePaginator::class, $result);
$this->assertCount(1, $result->items());
$this->assertEquals($this->channel->id, $result->items()[0]->id);
}
public function test_get_language_statistics(): void
{
$stats = $this->languageService->getLanguageStatistics($this->language->id);
$this->assertEquals(1, $stats->active_feeds_count);
$this->assertEquals(1, $stats->active_channels_count);
$this->assertEquals(0, $stats->active_routes_count);
}
public function test_get_common_languages_between_feed_and_channel(): void
{
$commonLanguages = $this->languageService->getCommonLanguages(
$this->feed->id,
$this->channel->id
);
$this->assertCount(1, $commonLanguages);
$this->assertEquals($this->language->id, $commonLanguages[0]['id']);
}
public function test_get_common_languages_with_no_match(): void
{
// Create another language and channel that doesn't match the feed
$otherLanguage = Language::factory()->create(['is_active' => true]);
$platformInstance = PlatformInstance::factory()->create();
$otherChannel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id,
'language_id' => $otherLanguage->id,
'is_active' => true
]);
$commonLanguages = $this->languageService->getCommonLanguages(
$this->feed->id,
$otherChannel->id
);
$this->assertCount(0, $commonLanguages);
}
public function test_can_language_be_used_for_routes(): void
{
$canBeUsed = $this->languageService->canLanguageBeUsedForRoutes($this->language->id);
$this->assertTrue($canBeUsed);
// Test with language that has no feeds or channels
$unusableLanguage = Language::factory()->create(['is_active' => true]);
$canBeUsed = $this->languageService->canLanguageBeUsedForRoutes($unusableLanguage->id);
$this->assertFalse($canBeUsed);
}
public function test_get_language_usage_summary(): void
{
$summary = $this->languageService->getLanguageUsageSummary();
$this->assertIsArray($summary);
$this->assertGreaterThanOrEqual(1, count($summary));
$languageData = collect($summary)->where('id', $this->language->id)->first();
$this->assertNotNull($languageData);
$this->assertEquals(1, $languageData['active_feeds_count']);
$this->assertEquals(1, $languageData['active_channels_count']);
$this->assertEquals(1, $languageData['can_create_routes']);
}
public function test_find_optimal_language(): void
{
$optimalLanguage = $this->languageService->findOptimalLanguage(
$this->feed->id,
$this->channel->id
);
$this->assertNotNull($optimalLanguage);
$this->assertEquals($this->language->id, $optimalLanguage['id']);
}
public function test_find_optimal_language_with_no_common_languages(): void
{
// Create another language and channel that doesn't match the feed
$otherLanguage = Language::factory()->create(['is_active' => true]);
$platformInstance = PlatformInstance::factory()->create();
$otherChannel = PlatformChannel::factory()->create([
'platform_instance_id' => $platformInstance->id,
'language_id' => $otherLanguage->id,
'is_active' => true
]);
$optimalLanguage = $this->languageService->findOptimalLanguage(
$this->feed->id,
$otherChannel->id
);
$this->assertNull($optimalLanguage);
}
public function test_validate_route_language(): void
{
$result = $this->languageService->validateRouteLanguage(
$this->feed->id,
$this->channel->id,
$this->language->id
);
$this->assertTrue($result['is_valid']);
$this->assertTrue($result['feed_supports_language']);
$this->assertTrue($result['channel_supports_language']);
}
public function test_batch_validate_routes(): void
{
$routeData = [
[
'feed_id' => $this->feed->id,
'platform_channel_id' => $this->channel->id,
'language_id' => $this->language->id
]
];
$results = $this->languageService->batchValidateRoutes($routeData);
$this->assertCount(1, $results);
$this->assertTrue($results[0]['is_valid']);
}
public function test_get_languages_with_counts(): void
{
$languages = $this->languageService->getLanguagesWithCounts();
$this->assertGreaterThanOrEqual(1, $languages->count());
$language = $languages->where('id', $this->language->id)->first();
$this->assertNotNull($language);
$this->assertEquals(1, $language->active_feeds_count);
$this->assertEquals(1, $language->active_channels_count);
}
}