src/Repositories/CourseRepository.php
<?php
namespace EscolaLms\Courses\Repositories;
use EscolaLms\Categories\Models\Category;
use EscolaLms\Core\Dtos\OrderDto;
use EscolaLms\Courses\Enum\CoursesPermissionsEnum;
use EscolaLms\Courses\Events\CoursedPublished;
use EscolaLms\Courses\Events\CourseTutorAssigned;
use EscolaLms\Courses\Events\CourseTutorUnassigned;
use EscolaLms\Courses\Models\Course;
use EscolaLms\Courses\Models\User;
use EscolaLms\Courses\Repositories\Contracts\CourseRepositoryContract;
use EscolaLms\Courses\Repositories\Contracts\LessonRepositoryContract;
use EscolaLms\Files\Helpers\FileHelper;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Application;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class CourseRepository
* @package EscolaLms\Courses\Repositories
* @version April 27, 2021, 11:19 am UTC
*/
class CourseRepository extends BaseRepository implements CourseRepositoryContract
{
private LessonRepositoryContract $lessonRepository;
/**
* @var array
*/
protected $fieldSearchable = [
'title',
'summary',
'image_path',
'video_path',
'duration',
'status',
'scorm_sco_id',
'poster_path',
'findable',
'target_group',
'teaser_url',
];
/**
* Return searchable fields
*
* @return array
*/
public function getFieldsSearchable()
{
return $this->fieldSearchable;
}
/**
* Configure the Model
**/
public function model()
{
return Course::class;
}
public function __construct(Application $app)
{
parent::__construct($app);
$this->lessonRepository = $app->make(LessonRepositoryContract::class);
}
/**
* Recursive flatten object by given $key
* @param object $input an object with children key
* @param string $key children key
*/
public static function flatten($input, $key)
{
$output = [];
foreach ($input as $object) {
$children = isset($object->$key) ? $object->$key : [];
$object->$key = [];
$output[] = $object;
$children = self::flatten($children, $key);
foreach ($children as $child) {
$output[] = $child;
}
}
return $output;
}
public function queryAll(): Builder
{
return $this->model->newQuery()
->select('courses.*', 'categories.name as category_name')
->leftJoin('category_course', 'category_course.course_id', '=', 'courses.id')
->leftJoin('categories', 'categories.id', '=', 'category_course.category_id');
}
public function allQueryBuilder(array $search = [], array $criteria = []): Builder
{
/** search main category and all subcategories */
if (isset($search['category_id'])) {
$collection = Category::where('id', $search['category_id'])->with('children')->get();
$flat = self::flatten($collection, 'children');
$flat_ids = array_map(fn($cat) => $cat->id, $flat);
unset($search['category_id']);
}
if (isset($search['categories'])) {
$flat_ids = [];
foreach ($search['categories'] as $category_id) {
$collection = Category::where('id', $category_id)->with('children')->get();
$flat = self::flatten($collection, 'children');
$flat_ids = array_merge($flat_ids, array_map(fn($cat) => $cat->id, $flat));
}
unset($search['categories']);
}
$query = $this->allQuery($search);
if (isset($flat_ids)) {
$query = $query->whereHas('categories', function (Builder $query) use ($flat_ids) {
$query->whereIn('categories.id', $flat_ids);
});
}
if (!empty($criteria)) {
$query = $this->applyCriteria($query, $criteria);
}
/** search by id in array */
if (isset($search['ids']) && !empty($search['ids'])) {
$query->whereIn('id', array_filter($search['ids'], 'is_numeric'));
}
/** search by TAG */
if (array_key_exists('tag', $search)) {
$tags = array_filter(is_array($search['tag']) ? $search['tag'] : [$search['tag']]);
if (!empty($tags)) {
$query->whereHas('tags', function (Builder $query) use ($tags) {
$firstTag = array_shift($tags);
$query->where('title', '=', $firstTag);
foreach ($tags as $tag) {
$query->orWhere('title', '=', $tag);
}
});
}
unset($search['tag']);
}
return isset($search['tag']) ? $query->with('tags') : $query;
}
/**
* Create model record
*
* @return Course
*/
public function create(array $input): Model
{
/** @var Course $model */
$model = $this->model->newInstance($input);
$model->save();
$update = [];
$courseId = $model->getKey();
if (isset($input['video'])) {
/** @var UploadedFile $video */
$video = $input['video'];
$update['video_path'] = $video->storePublicly("course/$courseId/videos");
}
if (isset($input['image'])) {
/** @var UploadedFile $image */
$image = $input['image'];
$update['image_path'] = $image->storePublicly("course/$courseId/images");
}
if (isset($input['poster'])) {
/** @var UploadedFile $poster */
$poster = $input['poster'];
$update['poster_path'] = $poster->storePublicly("course/$courseId/posters");
}
if (count($update)) {
$model->update($update);
}
if ($model->is_active && Auth::user()) {
event(new CoursedPublished(Auth::user(), $model));
}
$this->syncAuthors($model, $input['authors'] ?? (Auth::user() ? [Auth::id()] : []));
return $model;
}
/**
* Update model record for given id
*
* @return Course
*/
public function update(array $input, int $id): Model
{
$query = $this->model->newQuery();
/** @var Course $model */
$model = $query->findOrFail($id);
$isActive = $model->is_active;
if (isset($input['video_path'])) {
$input['video_path'] = Str::after($input['video_path'], env('AWS_ACCESS_KEY_ID') . '/');
}
if (isset($input['image_path'])) {
$input['image_path'] = Str::after($input['image_path'], env('AWS_ACCESS_KEY_ID') . '/');
}
if (isset($input['poster_path'])) {
$input['poster_path'] = Str::after($input['poster_path'], env('AWS_ACCESS_KEY_ID') . '/');
}
if (isset($input['video'])) {
$input['video_path'] = FileHelper::getFilePath($input['video'], "course/$id/videos");
}
if (isset($input['image'])) {
$input['image_path'] = FileHelper::getFilePath($input['image'], "course/$id/images");
}
if (isset($input['poster'])) {
$input['poster_path'] = FileHelper::getFilePath($input['poster'], "course/$id/posters");
}
if (isset($input['categories']) && is_array($input['categories'])) {
$model->categories()->sync($input['categories']);
}
if (isset($input['tags']) && is_array($input['tags'])) {
/** this is actually replacing the tags, even when you do send exactly the same */
$model->tags()->delete();
$tags = array_map(function ($tag) {
return ['title' => $tag];
}, $input['tags']);
$model->tags()->createMany($tags);
}
$model->fill($input);
$model->save();
if ($isActive !== $model->is_active && $model->is_active && Auth::user()) {
event(new CoursedPublished(Auth::user(), $model));
}
if (isset($input['authors'])) {
$this->syncAuthors($model, $input['authors']);
}
return $model;
}
public function syncAuthors(Course $course, array $authors = []): void
{
if (Auth::user() && !Auth::user()->can(CoursesPermissionsEnum::COURSE_UPDATE)) {
$authors = array_unique(array_merge($authors, $course->authors()->pluck('author_id')->all(), [Auth::id()])); // only admin can remove other authors?
}
$syncResults = $course->authors()->sync($authors);
foreach ($syncResults['attached'] as $attached) {
event(new CourseTutorAssigned(User::find($attached), $course));
}
foreach ($syncResults['detached'] as $detached) {
event(new CourseTutorUnassigned(User::find($detached), $course));
}
}
public function addAuthor(Course $course, User $author): void
{
if (!in_array($author->getKey(), $course->authors()->pluck('author_id')->all())) {
$course->authors()->attach($author->getKey());
event(new CourseTutorAssigned($author, $course));
}
}
public function removeAuthor(Course $course, User $author): void
{
if ($course->authors()->detach([$author->getKey()])) {
event(new CourseTutorUnassigned($author, $course));
$course->author_id = null;
}
}
public function getById(int $id): Course
{
return $this->model->newQuery()->find($id);
}
public function delete(int $id): ?bool
{
$course = $this->findWith($id, ['*'], ['lessons.topics']);
return !is_null($course) && $this->deleteModel($course);
}
public function deleteModel(Course $course): ?bool
{
foreach ($course->lessons as $lesson) {
$this->lessonRepository->deleteModel($lesson);
}
return $this->deleteAndClearStorage($course);
}
private function deleteAndClearStorage(Course $course): ?bool
{
if ($course->delete()) {
$path = Storage::path('course/' . $course->getKey());
try {
File::cleanDirectory($path);
Storage::deleteDirectory($path);
} catch (\Throwable $th) {
}
}
return true;
}
private function tutors(): Builder
{
return User::has('authoredCourses')->select(['id', 'first_name', 'last_name', 'email', 'path_avatar']);
}
public function findTutors(): Collection
{
return $this->tutors()->with(['interests'])->get();
}
public function findTutor($id): ?User
{
return $this->tutors()->where('id', $id)->first();
}
public function getAuthoredCourses(int $id, OrderDto $orderDto): Builder
{
return $this->model
->newQuery()
->whereHas('authors', function ($query) use ($id) {
$query->where('author_id', $id);
})
->orderBy($orderDto->getOrderBy() ?? 'created_at', $orderDto->getOrder() ?? 'desc');
}
}