234
common/Channels/BaseChannel.php
Executable file
234
common/Channels/BaseChannel.php
Executable file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Common\Core\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class BaseChannel extends BaseModel
|
||||
{
|
||||
const MODEL_TYPE = 'channel';
|
||||
protected $guarded = ['id'];
|
||||
protected $appends = ['model_type'];
|
||||
protected $hidden = ['pivot', 'internal'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'public' => 'boolean',
|
||||
'internal' => 'boolean',
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
// touch parent channels
|
||||
static::updated(function (self $channel) {
|
||||
$parentIds = DB::table('channelables')
|
||||
->where('channelable_type', static::MODEL_TYPE)
|
||||
->where('channelable_id', $channel->id)
|
||||
->pluck('channel_id');
|
||||
static::withoutEvents(function () use ($parentIds) {
|
||||
static::whereIn('id', $parentIds)->update([
|
||||
'updated_at' => Carbon::now(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
|
||||
public function users(): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'channelable')->withPivot([
|
||||
'id',
|
||||
'channelable_id',
|
||||
'order',
|
||||
]);
|
||||
}
|
||||
|
||||
public function channels(): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(static::class, 'channelable')->withPivot([
|
||||
'id',
|
||||
'channelable_id',
|
||||
'order',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getConfigAttribute(): ?array
|
||||
{
|
||||
return isset($this->attributes['config'])
|
||||
? json_decode($this->attributes['config'], true)
|
||||
: [];
|
||||
}
|
||||
|
||||
public function setConfigAttribute($value)
|
||||
{
|
||||
$this->attributes['config'] = is_array($value)
|
||||
? json_encode($value)
|
||||
: $value;
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'image' => $this->image,
|
||||
'description' =>
|
||||
$this->description ||
|
||||
Arr::get($this->attributes, 'config.seoDescription'),
|
||||
'model_type' => static::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
];
|
||||
}
|
||||
|
||||
public static function filterableFields(): array
|
||||
{
|
||||
return ['id', 'created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return static::MODEL_TYPE;
|
||||
}
|
||||
|
||||
public function loadRestrictionModel(string $urlParam = null)
|
||||
{
|
||||
$restriction = null;
|
||||
$modelName = $this->config['restriction'];
|
||||
$modelId = $this->config['restrictionModelId'] ?? null;
|
||||
$model = app(modelTypeToNamespace($modelName))->select([
|
||||
'id',
|
||||
'name',
|
||||
'display_name',
|
||||
]);
|
||||
|
||||
if ($modelId === 'urlParam' && $urlParam) {
|
||||
$restriction = $model->where('name', $urlParam)->first();
|
||||
} elseif (isset($modelId) && $modelId !== 'urlParam') {
|
||||
$restriction = $model->find($modelId);
|
||||
}
|
||||
|
||||
if ($restriction && !$restriction->display_name) {
|
||||
$restriction->display_name = ucwords($restriction->name);
|
||||
}
|
||||
|
||||
return $restriction;
|
||||
}
|
||||
|
||||
public function loadContent(array $params = [], self $parent = null): static
|
||||
{
|
||||
$channelContent = (new LoadChannelContent())->execute(
|
||||
$this,
|
||||
$params,
|
||||
$parent,
|
||||
);
|
||||
|
||||
if (Arr::get($params, 'normalizeContent') && $channelContent) {
|
||||
$channelContent->transform(function ($item) {
|
||||
$normalized = $item->toNormalizedArray();
|
||||
// needed in order to preserve "created_at" date when updating items
|
||||
if (isset($item->pivot)) {
|
||||
$normalized['created_at'] = $item->pivot->created_at;
|
||||
}
|
||||
return $normalized;
|
||||
});
|
||||
}
|
||||
|
||||
$this->setRelation('content', $channelContent);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function updateContentFromExternal(
|
||||
string $autoUpdateMethod = null,
|
||||
): void {
|
||||
$method =
|
||||
$autoUpdateMethod ?? Arr::get($this->config, 'autoUpdateMethod');
|
||||
|
||||
if (
|
||||
!$method ||
|
||||
Arr::get($this->config, 'contentType') !== 'autoUpdate'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $this->loadContentFromExternal($method);
|
||||
|
||||
// bail if we could not fetch any content
|
||||
if (!$content || $content->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// detach all channel items from the channel
|
||||
DB::table('channelables')
|
||||
->where(['channel_id' => $this->id])
|
||||
->delete();
|
||||
|
||||
// group content by model type (track, album, playlist etc)
|
||||
// and attach each group via its own separate relation
|
||||
$groupedContent = $content->groupBy('model_type');
|
||||
$groupedContent->each(function (Collection $contentGroup, $modelType) {
|
||||
$pivots = $contentGroup->mapWithKeys(
|
||||
fn($item, $index) => [$item['id'] => ['order' => $index]],
|
||||
);
|
||||
// track => tracks
|
||||
$relation = Str::plural($modelType);
|
||||
$this->$relation()->syncWithoutDetaching($pivots->toArray());
|
||||
});
|
||||
|
||||
// clear channel cache, it's based on updated_at timestamp
|
||||
$this->touch();
|
||||
}
|
||||
|
||||
public function shouldRestrictContent()
|
||||
{
|
||||
// when channel is set to auto update, content will be filtered when auto updating
|
||||
return Arr::get($this->config, 'contentType') !== 'autoUpdate' &&
|
||||
Arr::get($this->config, 'restriction');
|
||||
}
|
||||
|
||||
abstract protected function loadContentFromExternal(
|
||||
string $autoUpdateMethod,
|
||||
): Collection|array|null;
|
||||
|
||||
public function resolveRouteBinding($value, $field = null)
|
||||
{
|
||||
$type = request('channelType');
|
||||
if (ctype_digit($value)) {
|
||||
$channel = $this->when(
|
||||
$type,
|
||||
fn($q) => $q->where('type', $type),
|
||||
)->findOrFail($value);
|
||||
} else {
|
||||
$channel = $this->where('slug', $value)
|
||||
->when($type, fn($q) => $q->where('type', $type))
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
return $channel;
|
||||
}
|
||||
}
|
||||
36
common/Channels/ChannelContentOrderController.php
Executable file
36
common/Channels/ChannelContentOrderController.php
Executable file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ChannelContentOrderController extends BaseController
|
||||
{
|
||||
public function changeOrder(int $channelId)
|
||||
{
|
||||
$channel = Channel::findOrFail($channelId);
|
||||
|
||||
$this->authorize('update', $channel);
|
||||
|
||||
$data = request()->validate([
|
||||
'ids' => 'array|min:1',
|
||||
'ids.*' => 'int',
|
||||
'modelType' => 'required|string',
|
||||
]);
|
||||
|
||||
$queryPart = '';
|
||||
foreach ($data['ids'] as $order => $id) {
|
||||
$queryPart .= " when channelable_id=$id then $order";
|
||||
}
|
||||
|
||||
DB::table('channelables')
|
||||
->where('channel_id', $channel->id)
|
||||
->whereIn('channelable_id', $data['ids'])
|
||||
->where('channelable_type', $data['modelType'])
|
||||
->update(['order' => DB::raw("(case $queryPart end)")]);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
215
common/Channels/ChannelController.php
Executable file
215
common/Channels/ChannelController.php
Executable file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Http\Resources\ChannelResource;
|
||||
use App\Models\Channel;
|
||||
use App\Services\ChannelPresets;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Core\Prerender\Actions\ReplacePlaceholders;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ChannelController extends BaseController
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
$this->authorize('index', [Channel::class, 'channel']);
|
||||
|
||||
$pagination = (new PaginateChannels())->execute(request()->all());
|
||||
|
||||
return $this->success([
|
||||
'pagination' => $pagination,
|
||||
'presets' => (new ChannelPresets())->getAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Channel $channel)
|
||||
{
|
||||
$this->authorize('show', $channel);
|
||||
|
||||
$loader = request('loader', 'channelPage');
|
||||
|
||||
$params = request()->all();
|
||||
if ($loader === 'editUserListPage') {
|
||||
$params['normalizeContent'] = true;
|
||||
} elseif ($loader === 'editChannelPage') {
|
||||
$params['normalizeContent'] = true;
|
||||
$params['perPage'] = 100;
|
||||
}
|
||||
|
||||
$channel->loadContent($params);
|
||||
if (
|
||||
$loader === 'channelPage' &&
|
||||
$channel->shouldRestrictContent() &&
|
||||
!$channel->restriction
|
||||
) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$channel =
|
||||
$loader === 'channelPage' && class_exists(ChannelResource::class)
|
||||
? new ChannelResource($channel)
|
||||
: $channel;
|
||||
|
||||
// return only content for pagination
|
||||
if (request()->get('returnContentOnly')) {
|
||||
return [
|
||||
'pagination' => $channel->toArray(request())['content'],
|
||||
];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'channel' => $channel,
|
||||
'loader' => $loader,
|
||||
];
|
||||
|
||||
if ($loader === 'channelPage') {
|
||||
// used as default value during SSR in layout selector button
|
||||
$channel->config = array_merge($channel->config, [
|
||||
'selectedLayout' => Arr::get(
|
||||
$_COOKIE,
|
||||
"channel-layout-{$channel->config['contentModel']}",
|
||||
false,
|
||||
),
|
||||
'seoTitle' => isset($channel->config['seoTitle'])
|
||||
? app(ReplacePlaceholders::class)->execute(
|
||||
$channel->config['seoTitle'],
|
||||
$data,
|
||||
)
|
||||
: $channel->name,
|
||||
'seoDescription' => isset($channel->config['seoDescription'])
|
||||
? app(ReplacePlaceholders::class)->execute(
|
||||
$channel->config['seoDescription'],
|
||||
$data,
|
||||
)
|
||||
: $channel->description ?? $channel->name,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->renderClientOrApi([
|
||||
'pageName' => $loader === 'channelPage' ? 'channel-page' : null,
|
||||
'data' => [
|
||||
'channel' => $channel,
|
||||
'loader' => $loader,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CrupdateChannelRequest $request): Response
|
||||
{
|
||||
$this->authorize('store', [Channel::class, request('type', 'channel')]);
|
||||
|
||||
$channel = app(CrupdateChannel::class)->execute(
|
||||
$request->validationData(),
|
||||
);
|
||||
|
||||
return $this->success(['channel' => $channel]);
|
||||
}
|
||||
|
||||
public function update(
|
||||
Channel $channel,
|
||||
CrupdateChannelRequest $request,
|
||||
): Response {
|
||||
$this->authorize('update', $channel);
|
||||
|
||||
$channel = app(CrupdateChannel::class)->execute(
|
||||
$request->validationData(),
|
||||
$channel,
|
||||
);
|
||||
|
||||
return $this->success(['channel' => $channel]);
|
||||
}
|
||||
|
||||
public function destroy(string $ids): Response
|
||||
{
|
||||
$ids = explode(',', $ids);
|
||||
$channels = Channel::whereIn('id', $ids)->get();
|
||||
|
||||
$this->authorize('destroy', [Channel::class, $channels]);
|
||||
|
||||
app(DeleteChannels::class)->execute($channels);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function updateContent(Channel $channel): Response
|
||||
{
|
||||
$this->authorize('update', $channel);
|
||||
|
||||
if ($newConfig = request('channelConfig')) {
|
||||
$config = $channel->config;
|
||||
foreach ($newConfig as $key => $value) {
|
||||
$config[$key] = $value;
|
||||
}
|
||||
$channel->fill(['config' => $config])->save();
|
||||
}
|
||||
|
||||
$channel->updateContentFromExternal();
|
||||
$channel->loadContent(request()->all());
|
||||
|
||||
return $this->success([
|
||||
'channel' => $channel,
|
||||
]);
|
||||
}
|
||||
|
||||
public function searchForAddableContent(): Response
|
||||
{
|
||||
$namespace = modelTypeToNamespace(request('modelType'));
|
||||
$this->authorize('index', $namespace);
|
||||
|
||||
$builder = app($namespace);
|
||||
|
||||
if (request('query')) {
|
||||
$builder = $builder->mysqlSearch(request('query'));
|
||||
}
|
||||
|
||||
$results = $builder
|
||||
->take(20)
|
||||
->get()
|
||||
->filter(function ($result) {
|
||||
if (request('modelType') === 'channel') {
|
||||
// exclude user lists
|
||||
return $result->type === 'channel';
|
||||
}
|
||||
return true;
|
||||
})
|
||||
->map(fn($result) => $result->toNormalizedArray())
|
||||
->slice(0, request('limit', 10))
|
||||
->values();
|
||||
|
||||
return $this->success(['results' => $results]);
|
||||
}
|
||||
|
||||
public function applyPreset()
|
||||
{
|
||||
$this->authorize('destroy', Channel::class);
|
||||
|
||||
$data = request()->validate([
|
||||
'preset' => 'required|string',
|
||||
]);
|
||||
|
||||
$ids = Channel::where('type', 'channel')->pluck('id');
|
||||
DB::table('channelables')
|
||||
->whereIn('channel_id', $ids)
|
||||
->delete();
|
||||
Channel::whereIn('id', $ids)->delete();
|
||||
|
||||
(new ChannelPresets())->apply($data['preset']);
|
||||
|
||||
if (settings('homepage.type') === 'channels') {
|
||||
$homepage = Channel::where('name', 'homepage')->first();
|
||||
if ($homepage) {
|
||||
settings()->save(['homepage.value' => $homepage->id]);
|
||||
} else {
|
||||
settings()->save(['homepage.value' => null]);
|
||||
}
|
||||
}
|
||||
|
||||
Cache::flush();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
79
common/Channels/CrupdateChannel.php
Executable file
79
common/Channels/CrupdateChannel.php
Executable file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CrupdateChannel
|
||||
{
|
||||
public function execute($params, Channel $initialChannel = null): Channel
|
||||
{
|
||||
// can either specify channel model or namespace from which to instantiate
|
||||
if (!$initialChannel) {
|
||||
$channel = app(Channel::class)->newInstance([
|
||||
'user_id' => Auth::id(),
|
||||
]);
|
||||
} else {
|
||||
$channel = $initialChannel;
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'name' => $params['name'],
|
||||
'public' => $params['public'] ?? true,
|
||||
'internal' => $params['internal'] ?? false,
|
||||
'type' => $params['type'] ?? $channel->type ?? 'channel',
|
||||
'description' => $params['description'] ?? null,
|
||||
// merge old config so config that is not in crupdate channel form is not lost
|
||||
'config' => array_merge(
|
||||
$initialChannel['config'] ?? [],
|
||||
$params['config'],
|
||||
),
|
||||
];
|
||||
|
||||
if ($attributes['type'] !== 'list') {
|
||||
$attributes['slug'] = $params['slug'] ?? slugify($params['name']);
|
||||
}
|
||||
|
||||
$channel
|
||||
->fill(
|
||||
array_merge($attributes, [
|
||||
// make sure updated_at is always changed, event if model is
|
||||
// not dirty otherwise channel cache will not be cleared
|
||||
'updated_at' => now(),
|
||||
]),
|
||||
)
|
||||
->save();
|
||||
|
||||
if (
|
||||
$channel->config['contentType'] === 'manual' &&
|
||||
($channelContent = Arr::get($params, 'content.data'))
|
||||
) {
|
||||
// detach old channelables
|
||||
DB::table('channelables')
|
||||
->where('channel_id', $channel->id)
|
||||
->delete();
|
||||
|
||||
$pivots = collect($channelContent)
|
||||
->map(function ($item, $i) use ($channel) {
|
||||
return [
|
||||
'channel_id' => $channel->id,
|
||||
'channelable_id' => $item['id'],
|
||||
'channelable_type' => $item['model_type'],
|
||||
'created_at' => $item['created_at'] ?? now(),
|
||||
'order' => $i,
|
||||
];
|
||||
})
|
||||
->filter(function ($item) use ($channel) {
|
||||
// channels should not be attached to themselves
|
||||
return $item['channelable_type'] !== Channel::MODEL_TYPE ||
|
||||
$item['channelable_id'] !== $channel->id;
|
||||
});
|
||||
DB::table('channelables')->insert($pivots->toArray());
|
||||
}
|
||||
|
||||
return $channel;
|
||||
}
|
||||
}
|
||||
35
common/Channels/CrupdateChannelRequest.php
Executable file
35
common/Channels/CrupdateChannelRequest.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use Common\Core\BaseFormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CrupdateChannelRequest extends BaseFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
$required = $this->getMethod() === 'POST' ? 'required' : '';
|
||||
$ignore = $this->getMethod() === 'PUT' ? $this->route('channel')->id : '';
|
||||
|
||||
return [
|
||||
'name' => [
|
||||
$required, 'string', 'min:3', 'max:100',
|
||||
Rule::unique('channels')->ignore($ignore)
|
||||
],
|
||||
'description' => 'nullable|string|max:500',
|
||||
'public' => 'boolean',
|
||||
'content.data' => 'array',
|
||||
'config' => 'array',
|
||||
'type' => 'string',
|
||||
'config.autoUpdateMethod' => 'required_if:config.contentType,autoUpdate'
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'config.autoUpdateMethod' => __('Auto update method is required.'),
|
||||
];
|
||||
}
|
||||
}
|
||||
50
common/Channels/DeleteChannels.php
Executable file
50
common/Channels/DeleteChannels.php
Executable file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeleteChannels
|
||||
{
|
||||
public function execute(Collection $channels): int
|
||||
{
|
||||
if (
|
||||
$channels->some(
|
||||
fn(Channel $channel) => $channel->internal ||
|
||||
Arr::get($channel->config, 'preventDeletion'),
|
||||
)
|
||||
) {
|
||||
abort(422, __("Internal channels can't be deleted"));
|
||||
}
|
||||
|
||||
if (
|
||||
settings('homepage.type') === 'channels' &&
|
||||
$channels->contains('id', (int) settings('homepage.value'))
|
||||
) {
|
||||
abort(422, __('You can not delete the homepage channel'));
|
||||
}
|
||||
|
||||
$channelIds = $channels->pluck('id')->toArray();
|
||||
|
||||
// touch all channels that have channels we're deleting
|
||||
// nested so cache for them is cleared properly
|
||||
$parentChannelIds = DB::table('channelables')
|
||||
->where('channelable_type', Channel::MODEL_TYPE)
|
||||
->whereIn('channelable_id', $channelIds)
|
||||
->pluck('channel_id');
|
||||
Channel::whereIn('id', $parentChannelIds)->update([
|
||||
'updated_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
DB::table('channelables')
|
||||
->whereIn('channel_id', $channelIds)
|
||||
->delete();
|
||||
Channel::whereIn('id', $channelIds)->delete();
|
||||
|
||||
return count($channelIds);
|
||||
}
|
||||
}
|
||||
55
common/Channels/GenerateChannelsFromConfig.php
Executable file
55
common/Channels/GenerateChannelsFromConfig.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class GenerateChannelsFromConfig
|
||||
{
|
||||
public function execute(array $configPaths): Channel|null
|
||||
{
|
||||
foreach ($configPaths as $configPath) {
|
||||
$configs = json_decode(file_get_contents($configPath), true);
|
||||
|
||||
$createdChannels = [];
|
||||
|
||||
foreach ($configs as $config) {
|
||||
$nestedChannelSlugs = Arr::pull($config, 'nestedChannels');
|
||||
$presetDescription = Arr::pull($config, 'presetDescription');
|
||||
$config['config']['adminDescription'] = $presetDescription;
|
||||
$channel = Channel::create(
|
||||
array_merge($config, [
|
||||
'type' => 'channel',
|
||||
'public' => true,
|
||||
'internal' => $config['internal'] ?? false,
|
||||
]),
|
||||
);
|
||||
$createdChannels[] = [
|
||||
'parent' => $channel,
|
||||
'nestedChannelSlugs' => $nestedChannelSlugs,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($createdChannels as $createdChannel) {
|
||||
if (isset($createdChannel['nestedChannelSlugs'])) {
|
||||
foreach ($createdChannel['nestedChannelSlugs'] as $slug) {
|
||||
$nestedChannel = Channel::where('slug', $slug)->first();
|
||||
$createdChannel['parent']
|
||||
->channels()
|
||||
->attach($nestedChannel->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$homeChannel = Arr::first(
|
||||
$createdChannels,
|
||||
fn($c) => $c['parent']->slug === 'homepage',
|
||||
);
|
||||
|
||||
if (isset($homeChannel)) {
|
||||
return $homeChannel['parent'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
common/Channels/LoadChannelContent.php
Executable file
173
common/Channels/LoadChannelContent.php
Executable file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use BadMethodCallException;
|
||||
use Common\Core\Prerender\Actions\ReplacePlaceholders;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LoadChannelContent
|
||||
{
|
||||
public function execute(
|
||||
Channel $channel,
|
||||
array $params = [],
|
||||
Channel $parent = null,
|
||||
): ?AbstractPaginator {
|
||||
$params['perPage'] = $params['perPage'] ?? 50;
|
||||
$params['paginate'] = $this->resolvePaginateType($channel, $params);
|
||||
if (!isset($params['orderBy']) && !isset($params['order'])) {
|
||||
$params['order'] = Arr::get($channel->config, 'contentOrder');
|
||||
}
|
||||
|
||||
if (
|
||||
$channel->shouldRestrictContent() &&
|
||||
Arr::get($params, 'loader') !== 'editChannelPage'
|
||||
) {
|
||||
$this->applyRestriction($channel, $params, $parent);
|
||||
// If restriction could not be loaded bail. This is used to cancel content loading and return 404,
|
||||
// if, for example, loading genre channel, but specified genre does not exist.
|
||||
if (!$channel->restriction) {
|
||||
return new Paginator([], 15);
|
||||
}
|
||||
}
|
||||
|
||||
$contentType = Arr::get($channel->config, 'contentType');
|
||||
|
||||
if ($contentType === 'listAll') {
|
||||
return $this->paginateAllContentFromDatabase(
|
||||
$channel,
|
||||
$params,
|
||||
$parent,
|
||||
);
|
||||
} else {
|
||||
// only cache channels that have other nested channels as content
|
||||
$shouldCache =
|
||||
Arr::get($channel->config, 'contentModel') ===
|
||||
Channel::MODEL_TYPE;
|
||||
if (!$shouldCache) {
|
||||
return $this->loadCuratedContent($channel, $params);
|
||||
} else {
|
||||
$paramsKey = json_encode($params);
|
||||
return Cache::remember(
|
||||
// use "updated at" so channel changes from admin area will automatically
|
||||
// cause new cache item, without having to clear cache manually
|
||||
"channels.$channel->id.$channel->updated_at.$paramsKey",
|
||||
now()->addHours(24),
|
||||
fn() => $this->loadCuratedContent($channel, $params),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function paginateAllContentFromDatabase(
|
||||
Channel $channel,
|
||||
array $params,
|
||||
Channel $parent = null,
|
||||
): AbstractPaginator {
|
||||
$contentModel = Arr::get($channel->config, 'contentModel');
|
||||
$methodName = sprintf('all%s', ucfirst(Str::plural($contentModel)));
|
||||
// if channel specifies a method to load this model, use that
|
||||
if (method_exists($channel, $methodName)) {
|
||||
return $channel->{$methodName}($params, null, $parent);
|
||||
// otherwise do a basic pagination for the model
|
||||
} else {
|
||||
$namespace = modelTypeToNamespace($contentModel);
|
||||
$datasource = new Datasource(app($namespace)::query(), $params);
|
||||
return $datasource->paginate();
|
||||
}
|
||||
}
|
||||
|
||||
private function loadCuratedContent(
|
||||
Channel $channel,
|
||||
array $params,
|
||||
): AbstractPaginator {
|
||||
$contentModel = Arr::get($channel, 'config.contentModel');
|
||||
$methodName = sprintf('all%s', ucfirst(Str::plural($contentModel)));
|
||||
$builder = $channel->{Str::plural($contentModel)}();
|
||||
|
||||
// if channel specifies a method to load this model, use that
|
||||
if (method_exists($channel, $methodName)) {
|
||||
$pagination = $channel->{$methodName}($params, $builder);
|
||||
} else {
|
||||
$datasource = new Datasource($builder, $params);
|
||||
$order = $datasource->getOrder();
|
||||
|
||||
// get only column name, in case it's prefixed with table name
|
||||
if (last(explode('.', $order['col'])) === 'popularity') {
|
||||
$datasource->order = false;
|
||||
try {
|
||||
$builder->orderByPopularity($order['dir']);
|
||||
} catch (BadMethodCallException $e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
$pagination = $datasource->paginate();
|
||||
}
|
||||
|
||||
$pagination
|
||||
->filter(fn($model) => $model->pivot)
|
||||
->transform(function (Model $model) use ($channel, $params) {
|
||||
$model['channelable_id'] = $model->pivot->id;
|
||||
$model['channelable_order'] = $model->pivot->order;
|
||||
if ($model instanceof Channel) {
|
||||
$model->loadContent(
|
||||
array_merge($params, [
|
||||
// clear parent channel pagination params and only load 12 items per nested channel
|
||||
'perPage' => 12,
|
||||
'page' => 1,
|
||||
'paginate' => 'simple',
|
||||
// clear this so nested channel always uses sorting order set in that channel's config
|
||||
'order' => null,
|
||||
]),
|
||||
$channel,
|
||||
);
|
||||
}
|
||||
return $model;
|
||||
});
|
||||
return $pagination;
|
||||
}
|
||||
|
||||
private function applyRestriction(
|
||||
Channel $channel,
|
||||
array $params = [],
|
||||
Channel $parent = null,
|
||||
): void {
|
||||
$urlParam = Arr::get($params, 'restriction');
|
||||
$restriction =
|
||||
$parent->restriction ?? $channel->loadRestrictionModel($urlParam);
|
||||
if ($restriction) {
|
||||
$channel->setAttribute('restriction', $restriction);
|
||||
$channel->name =
|
||||
app(ReplacePlaceholders::class)->execute($channel->name, [
|
||||
'channel' => $channel,
|
||||
]) ?:
|
||||
$channel->name;
|
||||
}
|
||||
}
|
||||
|
||||
protected function resolvePaginateType(
|
||||
Channel $channel,
|
||||
array $params,
|
||||
): string {
|
||||
if (isset($params['paginate'])) {
|
||||
return $params['paginate'];
|
||||
}
|
||||
|
||||
if (isset($channel->config['paginationType'])) {
|
||||
return match ($channel->config['paginationType']) {
|
||||
'lengthAware' => 'lengthAware',
|
||||
// simple and infinite scroll
|
||||
default => 'simple',
|
||||
};
|
||||
}
|
||||
|
||||
return 'simple';
|
||||
}
|
||||
}
|
||||
25
common/Channels/LoadChannelMenuItems.php
Executable file
25
common/Channels/LoadChannelMenuItems.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class LoadChannelMenuItems
|
||||
{
|
||||
public function execute(): Collection
|
||||
{
|
||||
return Channel::limit(40)
|
||||
->where('type', 'channel')
|
||||
->get()
|
||||
->map(
|
||||
fn(Channel $channel) => [
|
||||
'label' => $channel->name,
|
||||
'action' => '/' . $channel->slug,
|
||||
'type' => 'route',
|
||||
'model_id' => $channel->id,
|
||||
'id' => $channel->id,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
40
common/Channels/PaginateChannels.php
Executable file
40
common/Channels/PaginateChannels.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
|
||||
class PaginateChannels
|
||||
{
|
||||
public function execute(array $params): AbstractPaginator
|
||||
{
|
||||
// $builder = Channel::where('type', 'channel')->whereNotExists(function (
|
||||
// $query,
|
||||
// ) {
|
||||
// $query
|
||||
// ->select('id')
|
||||
// ->from('channelables')
|
||||
// ->whereColumn('channelable_id', 'channels.id');
|
||||
// });
|
||||
|
||||
$builder = Channel::where('type', 'channel');
|
||||
|
||||
$paginator = new Datasource($builder, $params);
|
||||
|
||||
if (!isset($params['orderBy'])) {
|
||||
$builder->orderByRaw('`internal` = "1" desc, `updated_at` desc');
|
||||
$paginator->order = false;
|
||||
}
|
||||
|
||||
$pagination = $paginator->paginate();
|
||||
|
||||
$pagination->transform(function (Channel $channel) {
|
||||
$channel->makeVisible(['internal']);
|
||||
return $channel;
|
||||
});
|
||||
|
||||
return $pagination;
|
||||
}
|
||||
}
|
||||
25
common/Channels/UpdateAllChannelsContent.php
Executable file
25
common/Channels/UpdateAllChannelsContent.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Channels;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateAllChannelsContent extends Command
|
||||
{
|
||||
protected $signature = 'channels:update';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->info('Updating channels content...');
|
||||
|
||||
$channels = app(Channel::class)
|
||||
->where('type', 'channel')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
$this->withProgressBar($channels, function (Channel $channel) {
|
||||
$channel->updateContentFromExternal();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user