<?php
namespace App\Controller\Rest;
use App\Entity\Course;
use App\Repository\CourseFieldRepository;
use App\Repository\CourseRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Connection;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\View;
use FOS\RestBundle\Request\ParamFetcher;
use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/rest/course")
*/
class CourseRestController extends AbstractRestController
{
/**
* @Route("/", name="rest_course_index", methods="GET")
*
* @SWG\Get(
* produces={"application/json"},
*
* @SWG\Parameter(name="limit", in="query", type="integer"),
* @SWG\Parameter(name="offset", in="query", type="integer"),
* )
*
* @QueryParam(
* name="order",
* requirements="asc|desc",
* default="asc",
* description="Possible values: asc|desc",
* )
* @QueryParam(
* name="orderby",
* requirements="start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
* default="start",
* description="Possible values: start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
* )
* @QueryParam(
* name="limit",
* requirements="\d+",
* default="100",
* )
* @QueryParam(
* name="offset",
* requirements="\d+",
* default="0",
* )
* @QueryParam(
* map=true,
* name="speaker",
* description="Array of speaker ids.",
* requirements="\d+",
* default="0",
* )
* @QueryParam(
* map=true,
* name="venue",
* description="Array of venue ids.",
* requirements="\d+",
* default="0",
* )
* @QueryParam(
* map=true,
* name="course-series",
* description="Array of course-series ids.",
* requirements="\d+",
* default="0",
* )
* @QueryParam(
* map=true,
* name="course-nature",
* description="Array of course natures. Possible values: Course, CourseTemplate, CourseSubscription.",
* default=null,
* )
* @QueryParam(
* map=true,
* name="categories",
* description="Array of category ids.",
* requirements="\d+",
* default="0",
* )
* @QueryParam(
* name="showFields",
* description="show additional course fields",
* default="1"
* )
* @QueryParam(
* map=true,
* name="courseTypes",
* description="Array of courseType ids.",
* requirements="\d+",
* default="0",
* )
* @QueryParam(
* name="search",
* default="",
* description="Searches these fields: course title, subtitle, description, number | venue name, city, street"
* )
* @QueryParam(
* name="isBookable",
* default="0",
* description="Only returns bookable courses"
* )
* @QueryParam(
* name="startAfterDate",
* default="",
* description="Only returns courses that start after given date"
* )
* @QueryParam(
* name="withOccurrences",
* default="1",
* description="Return courses with occurrences"
* )
* @QueryParam(
* map=true,
* name="withProvider",
* default="1",
* description="Return courses with provider"
* )
*
* @SWG\Response(
* response=200,
* description="Returns the overview of course items as json.",
*
* @SWG\Schema(
* type="array",
*
* @SWG\Items(ref=@Model(type=Course::class, groups={"public"}))
* )
* )
*
* @SWG\Tag(name="rest")
*
* @View(serializerGroups={"public"})
*/
public function index(ParamFetcher $paramFetcher, CourseRepository $courseRepository, CourseFieldRepository $courseFieldRepository)
{
$order = $paramFetcher->get('order');
$orderby = $paramFetcher->get('orderby');
$limit = $paramFetcher->get('limit');
$offset = $paramFetcher->get('offset');
$speaker = $paramFetcher->get('speaker');
$venue = $paramFetcher->get('venue');
$courseSeries = $paramFetcher->get('course-series');
$categories = $paramFetcher->get('categories');
$search = $paramFetcher->get('search');
$courseNature = $paramFetcher->get('course-nature');
$courseTypes = $paramFetcher->get('courseTypes');
$isBookable = $paramFetcher->get('isBookable');
$startAfterDate = $paramFetcher->get('startAfterDate');
$showFields = $paramFetcher->get('showFields');
$withOccurrences = $paramFetcher->get('withOccurrences');
$withProvider = $paramFetcher->get('withProvider');
$filters = ['published' => true];
$filters['endAfterDate'] = new \DateTime();
if (!empty($speaker)) {
$filters['speaker'] = $speaker;
}
if (!empty($venue)) {
$filters['venue'] = $venue;
}
if (!empty($courseSeries)) {
$filters['courseSeries'] = $courseSeries;
}
if (!empty($categories)) {
$filters['categories'] = $categories;
}
if (!empty($courseTypes)) {
$filters['courseTypes'] = $courseTypes;
}
if (!empty($search)) {
$filters['search'] = $search;
}
if (!empty($courseNature)) {
$filters['courseNature'] = $courseNature;
}
if (!empty($isBookable)) {
$filters['isBookable'] = $isBookable;
}
if (!empty($startAfterDate)) {
$filters['startAfterDate'] = $startAfterDate;
}
if (!empty($withOccurrences)) {
$filters['withOccurrences'] = $withOccurrences;
}
if (!empty($withProvider)) {
$filters['withProvider'] = true; // sorgt für JOIN + addSelect im Repository
}
$courses = $courseRepository->getCoursesByClient($this->getCurrentClient(), $limit, $offset, $order, $orderby, null, null, $courseTypes, $filters);
if (empty($paramFetcher->get('withOccurrences'))) {
foreach ($courses as $course) {
$course->setOccurrences(null);
$course->setFields($courseFieldRepository->getFieldsWithxCourseId($course->getId()));
}
} else {
foreach ($courses as $course) {
$course->setOccurrences($course->getOccurrences(true));
$course->setFields($courseFieldRepository->getFieldsWithxCourseId($course->getId()));
}
}
foreach ($courses as $c) {
error_log(sprintf('[REST index] course #%d providers=%d', $c->getId(), $c->getCourseProviders()->count()));
}
return $courses;
}
/**
* @Route("/{id}", name="rest_course_show", methods="GET", requirements={"id"="\d+"})
*
* @SWG\Get(
* produces={"application/json"},
* )
*
* @SWG\Parameter(name="id", in="path", required=true, type="integer")
*
* @QueryParam(
* name="showPairings",
* description="show possible slot pairings",
* default="0"
* )
* @QueryParam(
* name="showFields",
* description="show additional course fields",
* default="1"
* )
* @QueryParam(
* name="isBookable",
* default="1",
* description="Only returns bookable courses"
* )
* @QueryParam(
* name="withOccurrences",
* default="1",
* description="Return courses with occurrences"
* )
* @QueryParam(
* map=true,
* name="withProvider",
* default="1",
* description="Return courses with provider"
* )
*
* @SWG\Response(
* response=200,
* description="Returns the course by given id as json.",
*
* @Model(type=Course::class, groups={"public", "detail"})
* )
*
* @SWG\Tag(name="rest")
*
* @View(serializerGroups={"public", "detail"})
*/
public function show(
int $id,
ParamFetcher $paramFetcher,
CourseRepository $courseRepository,
Connection $connection,
CourseFieldRepository $courseFieldRepository,
) {
// Load course tenant-aware
$course = $courseRepository->findOneBy([
'id' => $id,
'client' => $this->getCurrentClient()
]);
if (!$course) {
throw $this->createNotFoundException('Course not found or access denied');
}
// Security: Check client ownership via CourseSecurityVoter
$this->denyAccessUnlessGranted('view', $course);
$isBookable = filter_var($paramFetcher->get('isBookable', true), FILTER_VALIDATE_BOOLEAN);
$showPairings = $paramFetcher->get('showPairings');
$showFields = $paramFetcher->get('showFields');
$withProvider = $paramFetcher->get('withProvider');
// Filter occurences/times
if ('CourseTemplate' == $course->getCourseNature()) {
$occurrences = new ArrayCollection();
foreach ($course->getOccurrences() as $occurrence) {
if (1 != $occurrence->getPublished()) {
continue;
}
// Load providers for this occurrence if requested
if ($withProvider) {
$providerSql = 'SELECT p.* FROM provider p
INNER JOIN course_provider cp ON p.id = cp.provider_id
WHERE cp.course_occurrence_id = ?';
$providers = $connection->fetchAllAssociative($providerSql, [$occurrence->getId()]);
$occurrence->setProviders($providers);
}
$times = new ArrayCollection();
if ($showPairings) {
$sql = 'SELECT
cot.id,
cx.title,
cot.occurrenceId,
cot.start,
cot.end,
DATE_FORMAT(cot.start, "%Y-%m-%d") as date,
DATE_FORMAT(cot.start, "%H:%i") as startTime,
DATE_FORMAT(cot.end, "%H:%i") as endTime
FROM
course_occurrence co,
course_occurrence_time cot,
course c,
course cx
WHERE
co.id != '.$occurrence->getId().' AND
c.id = '.$occurrence->getCourse()->getId().' AND
cx.series_id = c.series_id AND
cx.id = co.courseId AND
cot.occurrenceId = co.id AND
co.published = 1';
$result = $connection->fetchAllAssociative($sql);
}
foreach ($occurrence->getTimes() as $time) {
// Drop times which are not available or requestable
if ('NotAvailable' == $time->getAvailability()) {
continue;
}
// Drop times which are booked already
if ('Booked' == $time->getAvailability()) {
continue;
}
// Drop past times
if ($time->getStart()->getTimestamp() < $_SERVER['REQUEST_TIME']) {
continue;
}
if ($showPairings) {
$date = $time->getStart()->format('Y-m-d');
$pairings = [];
foreach ($result as $pairCheck) {
// Skip times on different days
if ($date != $pairCheck['date']) {
continue;
}
// Skip times that overlap
if (!(strtotime($pairCheck['end']) <= $time->getStart()->getTimestamp() or strtotime($pairCheck['start']) >= $time->getEnd()->getTimestamp())) {
continue;
}
$pairings[] = $pairCheck;
}
$time->setPairings($pairings);
}
$times->add($time);
}
// Skip occurences with no available time slots left
if (0 == $times->count()) {
continue;
}
$occurrence->setTimes($times);
$occurrence->setProviders($providers);
$occurrences->add($occurrence);
}
$course->setOccurrences($occurrences);
} else {
$course->setOccurrences($course->getOccurrences(true, false));
}
// Felder laden
$course->setFields($courseFieldRepository->getFieldsWithxCourseId($course->getId()));
return $course;
}
/**
* @Route("/{id}/related", name="rest_course_related", methods="GET", requirements={"id"="\d+"})
*
* @SWG\Get(
* produces={"application/json"},
*
* @SWG\Parameter(name="id", in="path", required=true, type="integer"),
* @SWG\Parameter(name="limit", in="query", type="integer"),
* @SWG\Parameter(name="offset", in="query", type="integer"),
* @SWG\Parameter(name="showPastEvents", in="query", type="integer"),
* )
*
* @QueryParam(
* name="order",
* requirements="asc|desc",
* default="asc",
* description="Possible values: asc|desc",
* )
* @QueryParam(
* name="orderby",
* requirements="start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
* default="start",
* description="Possible values: start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
* )
* @QueryParam(
* name="limit",
* requirements="\d+",
* default="3",
* )
* @QueryParam(
* name="offset",
* requirements="\d+",
* default="0",
* )
* @QueryParam(
* name="showPastEvents",
* requirements="\d+",
* default="0",
* )
*
* @SWG\Response(
* response=200,
* description="Returns course items related to given course as json.",
*
* @SWG\Schema(
* type="array",
*
* @SWG\Items(ref=@Model(type=Course::class, groups={"public"}))
* )
* )
*
* @SWG\Tag(name="rest")
*
* @View(serializerGroups={"public"})
*/
public function related(int $id, ParamFetcher $paramFetcher, CourseRepository $repo)
{
// Load course tenant-aware
$course = $repo->findOneBy([
'id' => $id,
'client' => $this->getCurrentClient()
]);
if (!$course) {
throw $this->createNotFoundException('Course not found or access denied');
}
// Security: Check client ownership
$this->denyAccessUnlessGranted('view', $course);
$order = $paramFetcher->get('order');
$orderby = $paramFetcher->get('orderby');
$limit = $paramFetcher->get('limit');
$showPastEvents = $paramFetcher->get('showPastEvents');
$offset = $paramFetcher->get('offset');
$filters = ['published' => true, 'isBookable' => true];
if (empty($paramFetcher->get('showPastEvents'))) {
$filters['endAfterDate'] = new \DateTime();
}
if (1 == $showPastEvents) {
$date = new \DateTime();
} else {
null == $date;
}
// Security: Check client ownership via CourseSecurityVoter
$this->denyAccessUnlessGranted('view', $course);
$relatedCourses = $repo->getCoursesByClient($this->getCurrentClient(), $limit, $offset, $order, $orderby, $date, $course->getCategory(), null, $filters);
return $relatedCourses;
}
}