src/Controller/Rest/CourseRestController.php line 20

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Rest;
  3. use App\Entity\Course;
  4. use App\Repository\CourseFieldRepository;
  5. use App\Repository\CourseRepository;
  6. use Doctrine\Common\Collections\ArrayCollection;
  7. use Doctrine\DBAL\Connection;
  8. use FOS\RestBundle\Controller\Annotations\QueryParam;
  9. use FOS\RestBundle\Controller\Annotations\View;
  10. use FOS\RestBundle\Request\ParamFetcher;
  11. use Nelmio\ApiDocBundle\Annotation\Model;
  12. use Swagger\Annotations as SWG;
  13. use Symfony\Component\Routing\Annotation\Route;
  14. /**
  15.  * @Route("/rest/course")
  16.  */
  17. class CourseRestController extends AbstractRestController
  18. {
  19.     /**
  20.      * @Route("/", name="rest_course_index", methods="GET")
  21.      *
  22.      * @SWG\Get(
  23.      *     produces={"application/json"},
  24.      *
  25.      *     @SWG\Parameter(name="limit", in="query", type="integer"),
  26.      *     @SWG\Parameter(name="offset", in="query", type="integer"),
  27.      * )
  28.      *
  29.      * @QueryParam(
  30.      *     name="order",
  31.      *     requirements="asc|desc",
  32.      *     default="asc",
  33.      *     description="Possible values: asc|desc",
  34.      * )
  35.      * @QueryParam(
  36.      *     name="orderby",
  37.      *     requirements="start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
  38.      *     default="start",
  39.      *     description="Possible values: start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
  40.      * )
  41.      * @QueryParam(
  42.      *     name="limit",
  43.      *     requirements="\d+",
  44.      *     default="100",
  45.      * )
  46.      * @QueryParam(
  47.      *     name="offset",
  48.      *     requirements="\d+",
  49.      *     default="0",
  50.      * )
  51.      * @QueryParam(
  52.      *     map=true,
  53.      *     name="speaker",
  54.      *     description="Array of speaker ids.",
  55.      *     requirements="\d+",
  56.      *     default="0",
  57.      * )
  58.      * @QueryParam(
  59.      *     map=true,
  60.      *     name="venue",
  61.      *     description="Array of venue ids.",
  62.      *     requirements="\d+",
  63.      *     default="0",
  64.      * )
  65.      * @QueryParam(
  66.      *     map=true,
  67.      *     name="course-series",
  68.      *     description="Array of course-series ids.",
  69.      *     requirements="\d+",
  70.      *     default="0",
  71.      * )
  72.      * @QueryParam(
  73.      *     map=true,
  74.      *     name="course-nature",
  75.      *     description="Array of course natures. Possible values: Course, CourseTemplate, CourseSubscription.",
  76.      *     default=null,
  77.      * )
  78.      * @QueryParam(
  79.      *     map=true,
  80.      *     name="categories",
  81.      *     description="Array of category ids.",
  82.      *     requirements="\d+",
  83.      *     default="0",
  84.      * )
  85.      * @QueryParam(
  86.      *     name="showFields",
  87.      *     description="show additional course fields",
  88.      *     default="1"
  89.      * )
  90.      * @QueryParam(
  91.      *     map=true,
  92.      *     name="courseTypes",
  93.      *     description="Array of courseType ids.",
  94.      *     requirements="\d+",
  95.      *     default="0",
  96.      * )
  97.      * @QueryParam(
  98.      *     name="search",
  99.      *     default="",
  100.      *     description="Searches these fields: course title, subtitle, description, number | venue name, city, street"
  101.      * )
  102.      * @QueryParam(
  103.      *     name="isBookable",
  104.      *     default="0",
  105.      *     description="Only returns bookable courses"
  106.      * )
  107.      * @QueryParam(
  108.      *     name="startAfterDate",
  109.      *     default="",
  110.      *     description="Only returns courses that start after given date"
  111.      * )
  112.      * @QueryParam(
  113.      *     name="withOccurrences",
  114.      *     default="1",
  115.      *     description="Return courses with occurrences"
  116.      * )
  117.      *  @QueryParam(
  118.      *     map=true,
  119.      *     name="withProvider",
  120.      *     default="1",
  121.      *     description="Return courses with provider"
  122.      * )
  123.      *
  124.      * @SWG\Response(
  125.      *     response=200,
  126.      *     description="Returns the overview of course items as json.",
  127.      *
  128.      *     @SWG\Schema(
  129.      *         type="array",
  130.      *
  131.      *         @SWG\Items(ref=@Model(type=Course::class, groups={"public"}))
  132.      *     )
  133.      * )
  134.      *
  135.      * @SWG\Tag(name="rest")
  136.      *
  137.      * @View(serializerGroups={"public"})
  138.      */
  139.     public function index(ParamFetcher $paramFetcherCourseRepository $courseRepositoryCourseFieldRepository $courseFieldRepository)
  140.     {
  141.         $order $paramFetcher->get('order');
  142.         $orderby $paramFetcher->get('orderby');
  143.         $limit $paramFetcher->get('limit');
  144.         $offset $paramFetcher->get('offset');
  145.         $speaker $paramFetcher->get('speaker');
  146.         $venue $paramFetcher->get('venue');
  147.         $courseSeries $paramFetcher->get('course-series');
  148.         $categories $paramFetcher->get('categories');
  149.         $search $paramFetcher->get('search');
  150.         $courseNature $paramFetcher->get('course-nature');
  151.         $courseTypes $paramFetcher->get('courseTypes');
  152.         $isBookable $paramFetcher->get('isBookable');
  153.         $startAfterDate $paramFetcher->get('startAfterDate');
  154.         $showFields $paramFetcher->get('showFields');
  155.         $withOccurrences $paramFetcher->get('withOccurrences');
  156.         $withProvider $paramFetcher->get('withProvider');
  157.         $filters = ['published' => true];
  158.         $filters['endAfterDate'] = new \DateTime();
  159.         if (!empty($speaker)) {
  160.             $filters['speaker'] = $speaker;
  161.         }
  162.         if (!empty($venue)) {
  163.             $filters['venue'] = $venue;
  164.         }
  165.         if (!empty($courseSeries)) {
  166.             $filters['courseSeries'] = $courseSeries;
  167.         }
  168.         if (!empty($categories)) {
  169.             $filters['categories'] = $categories;
  170.         }
  171.         if (!empty($courseTypes)) {
  172.             $filters['courseTypes'] = $courseTypes;
  173.         }
  174.         if (!empty($search)) {
  175.             $filters['search'] = $search;
  176.         }
  177.         if (!empty($courseNature)) {
  178.             $filters['courseNature'] = $courseNature;
  179.         }
  180.         if (!empty($isBookable)) {
  181.             $filters['isBookable'] = $isBookable;
  182.         }
  183.         if (!empty($startAfterDate)) {
  184.             $filters['startAfterDate'] = $startAfterDate;
  185.         }
  186.         if (!empty($withOccurrences)) {
  187.             $filters['withOccurrences'] = $withOccurrences;
  188.         }
  189.         if (!empty($withProvider)) {
  190.             $filters['withProvider'] = true// sorgt für JOIN + addSelect im Repository
  191.         }
  192.         $courses $courseRepository->getCoursesByClient($this->getCurrentClient(), $limit$offset$order$orderbynullnull$courseTypes$filters);
  193.         if (empty($paramFetcher->get('withOccurrences'))) {
  194.             foreach ($courses as $course) {
  195.                 $course->setOccurrences(null);
  196.                 $course->setFields($courseFieldRepository->getFieldsWithxCourseId($course->getId()));
  197.             }
  198.         } else {
  199.             foreach ($courses as $course) {
  200.                 $course->setOccurrences($course->getOccurrences(true));
  201.                 $course->setFields($courseFieldRepository->getFieldsWithxCourseId($course->getId()));
  202.             }
  203.         }
  204.         foreach ($courses as $c) {
  205.             error_log(sprintf('[REST index] course #%d providers=%d'$c->getId(), $c->getCourseProviders()->count()));
  206.         }
  207.         return $courses;
  208.     }
  209.     /**
  210.      * @Route("/{id}", name="rest_course_show", methods="GET", requirements={"id"="\d+"})
  211.      *
  212.      * @SWG\Get(
  213.      *     produces={"application/json"},
  214.      * )
  215.      *
  216.      * @SWG\Parameter(name="id", in="path", required=true, type="integer")
  217.      *
  218.      * @QueryParam(
  219.      *     name="showPairings",
  220.      *     description="show possible slot pairings",
  221.      *     default="0"
  222.      * )
  223.      * @QueryParam(
  224.      *     name="showFields",
  225.      *     description="show additional course fields",
  226.      *     default="1"
  227.      * )
  228.      * @QueryParam(
  229.      *     name="isBookable",
  230.      *     default="1",
  231.      *     description="Only returns bookable courses"
  232.      * )
  233.      *  @QueryParam(
  234.      *     name="withOccurrences",
  235.      *     default="1",
  236.      *     description="Return courses with occurrences"
  237.      * )
  238.      *  @QueryParam(
  239.      *     map=true,
  240.      *     name="withProvider",
  241.      *     default="1",
  242.      *     description="Return courses with provider"
  243.      * )
  244.      *
  245.      * @SWG\Response(
  246.      *     response=200,
  247.      *     description="Returns the course by given id as json.",
  248.      *
  249.      *     @Model(type=Course::class, groups={"public", "detail"})
  250.      * )
  251.      *
  252.      * @SWG\Tag(name="rest")
  253.      *
  254.      * @View(serializerGroups={"public",  "detail"})
  255.      */
  256.     public function show(
  257.         int $id,
  258.         ParamFetcher $paramFetcher,
  259.         CourseRepository $courseRepository,
  260.         Connection $connection,
  261.         CourseFieldRepository $courseFieldRepository,
  262.     ) {
  263.         // Load course tenant-aware
  264.         $course $courseRepository->findOneBy([
  265.             'id' => $id,
  266.             'client' => $this->getCurrentClient()
  267.         ]);
  268.         
  269.         if (!$course) {
  270.             throw $this->createNotFoundException('Course not found or access denied');
  271.         }
  272.         
  273.         // Security: Check client ownership via CourseSecurityVoter
  274.         $this->denyAccessUnlessGranted('view'$course);
  275.         $isBookable filter_var($paramFetcher->get('isBookable'true), FILTER_VALIDATE_BOOLEAN);
  276.         $showPairings $paramFetcher->get('showPairings');
  277.         $showFields $paramFetcher->get('showFields');
  278.         $withProvider $paramFetcher->get('withProvider');
  279.         // Filter occurences/times
  280.         if ('CourseTemplate' == $course->getCourseNature()) {
  281.             $occurrences = new ArrayCollection();
  282.             foreach ($course->getOccurrences() as $occurrence) {
  283.                 if (!= $occurrence->getPublished()) {
  284.                     continue;
  285.                 }
  286.                 // Load providers for this occurrence if requested
  287.                 if ($withProvider) {
  288.                     $providerSql 'SELECT p.* FROM provider p 
  289.                                           INNER JOIN course_provider cp ON p.id = cp.provider_id 
  290.                                           WHERE cp.course_occurrence_id = ?';
  291.                     $providers $connection->fetchAllAssociative($providerSql, [$occurrence->getId()]);
  292.                     $occurrence->setProviders($providers);
  293.                 }
  294.                 $times = new ArrayCollection();
  295.                 if ($showPairings) {
  296.                     $sql 'SELECT
  297.                                  cot.id,
  298.                                  cx.title,
  299.                                  cot.occurrenceId,
  300.                                  cot.start,    
  301.                                  cot.end,
  302.                                 
  303.                                  DATE_FORMAT(cot.start, "%Y-%m-%d") as date,
  304.                                  DATE_FORMAT(cot.start, "%H:%i") as startTime,
  305.                                  DATE_FORMAT(cot.end, "%H:%i") as endTime
  306.                             FROM 
  307.                                  course_occurrence co,
  308.                                  course_occurrence_time cot,
  309.                                  course c,
  310.                                  course cx
  311.                             WHERE
  312.              
  313.                                  co.id != '.$occurrence->getId().' AND
  314.                                  c.id = '.$occurrence->getCourse()->getId().' AND
  315.                                  cx.series_id = c.series_id AND
  316.                             
  317.                                  cx.id = co.courseId AND
  318.                                 cot.occurrenceId = co.id AND
  319.                                  co.published = 1';
  320.                     $result $connection->fetchAllAssociative($sql);
  321.                 }
  322.                 foreach ($occurrence->getTimes() as $time) {
  323.                     // Drop times which are not available or requestable
  324.                     if ('NotAvailable' == $time->getAvailability()) {
  325.                         continue;
  326.                     }
  327.                     // Drop times which are booked already
  328.                     if ('Booked' == $time->getAvailability()) {
  329.                         continue;
  330.                     }
  331.                     // Drop past times
  332.                     if ($time->getStart()->getTimestamp() < $_SERVER['REQUEST_TIME']) {
  333.                         continue;
  334.                     }
  335.                     if ($showPairings) {
  336.                         $date $time->getStart()->format('Y-m-d');
  337.                         $pairings = [];
  338.                         foreach ($result as $pairCheck) {
  339.                             // Skip times on different days
  340.                             if ($date != $pairCheck['date']) {
  341.                                 continue;
  342.                             }
  343.                             // Skip times that overlap
  344.                             if (!(strtotime($pairCheck['end']) <= $time->getStart()->getTimestamp() or strtotime($pairCheck['start']) >= $time->getEnd()->getTimestamp())) {
  345.                                 continue;
  346.                             }
  347.                             $pairings[] = $pairCheck;
  348.                         }
  349.                         $time->setPairings($pairings);
  350.                     }
  351.                     $times->add($time);
  352.                 }
  353.                 // Skip occurences with no available time slots left
  354.                 if (== $times->count()) {
  355.                     continue;
  356.                 }
  357.                 $occurrence->setTimes($times);
  358.                 $occurrence->setProviders($providers);
  359.                 $occurrences->add($occurrence);
  360.             }
  361.             $course->setOccurrences($occurrences);
  362.         } else {
  363.             $course->setOccurrences($course->getOccurrences(truefalse));
  364.         }
  365.         // Felder laden
  366.         $course->setFields($courseFieldRepository->getFieldsWithxCourseId($course->getId()));
  367.         return $course;
  368.     }
  369.     /**
  370.      * @Route("/{id}/related", name="rest_course_related", methods="GET", requirements={"id"="\d+"})
  371.      *
  372.      * @SWG\Get(
  373.      *     produces={"application/json"},
  374.      *
  375.      *     @SWG\Parameter(name="id", in="path", required=true, type="integer"),
  376.      *     @SWG\Parameter(name="limit", in="query", type="integer"),
  377.      *     @SWG\Parameter(name="offset", in="query", type="integer"),
  378.      *     @SWG\Parameter(name="showPastEvents", in="query", type="integer"),
  379.      * )
  380.      *
  381.      * @QueryParam(
  382.      *     name="order",
  383.      *     requirements="asc|desc",
  384.      *     default="asc",
  385.      *     description="Possible values: asc|desc",
  386.      * )
  387.      * @QueryParam(
  388.      *     name="orderby",
  389.      *     requirements="start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
  390.      *     default="start",
  391.      *     description="Possible values: start|title|price|number|materialCost|targetAgeMin|targetAgeMax",
  392.      * )
  393.      * @QueryParam(
  394.      *     name="limit",
  395.      *     requirements="\d+",
  396.      *     default="3",
  397.      * )
  398.      * @QueryParam(
  399.      *     name="offset",
  400.      *     requirements="\d+",
  401.      *     default="0",
  402.      * )
  403.      * @QueryParam(
  404.      *     name="showPastEvents",
  405.      *     requirements="\d+",
  406.      *     default="0",
  407.      * )
  408.      *
  409.      * @SWG\Response(
  410.      *     response=200,
  411.      *     description="Returns course items related to given course as json.",
  412.      *
  413.      *     @SWG\Schema(
  414.      *         type="array",
  415.      *
  416.      *         @SWG\Items(ref=@Model(type=Course::class, groups={"public"}))
  417.      *     )
  418.      * )
  419.      *
  420.      * @SWG\Tag(name="rest")
  421.      *
  422.      * @View(serializerGroups={"public"})
  423.      */
  424.     public function related(int $idParamFetcher $paramFetcherCourseRepository $repo)
  425.     {
  426.         // Load course tenant-aware
  427.         $course $repo->findOneBy([
  428.             'id' => $id,
  429.             'client' => $this->getCurrentClient()
  430.         ]);
  431.         
  432.         if (!$course) {
  433.             throw $this->createNotFoundException('Course not found or access denied');
  434.         }
  435.         
  436.         // Security: Check client ownership
  437.         $this->denyAccessUnlessGranted('view'$course);
  438.         $order $paramFetcher->get('order');
  439.         $orderby $paramFetcher->get('orderby');
  440.         $limit $paramFetcher->get('limit');
  441.         $showPastEvents $paramFetcher->get('showPastEvents');
  442.         $offset $paramFetcher->get('offset');
  443.         $filters = ['published' => true'isBookable' => true];
  444.         if (empty($paramFetcher->get('showPastEvents'))) {
  445.             $filters['endAfterDate'] = new \DateTime();
  446.         }
  447.         if (== $showPastEvents) {
  448.             $date = new \DateTime();
  449.         } else {
  450.             null == $date;
  451.         }
  452.         // Security: Check client ownership via CourseSecurityVoter
  453.         $this->denyAccessUnlessGranted('view'$course);
  454.         $relatedCourses $repo->getCoursesByClient($this->getCurrentClient(), $limit$offset$order$orderby$date$course->getCategory(), null$filters);
  455.         return $relatedCourses;
  456.     }
  457. }