      1: <?php
      2:
      3: /*
      4:  * This file is part of the CRUDlex package.
      5:  *
      6:  * (c) Philip Lehmann-Böhm <philip@philiplb.de>
      7:  *
      8:  * For the full copyright and license information, please view the LICENSE
      9:  * file that was distributed with this source code.
     10:  */
     11:
     12: namespace CRUDlex;
     13:
     14: use Silex\Application;
     15: use Silex\ControllerProviderInterface;
     16: use Symfony\Component\HttpFoundation\Response;
     17: use Symfony\Component\HttpFoundation\StreamedResponse;
     18:
     19: use CRUDlex\CRUDEntity;
     20:
     21: /**
     22:  * This is the ControllerProvider offering all CRUD pages.
     23:  *
     24:  * It offers this routes:
     25:  *
     26:  * "/resource/static" serving static resources
     27:  *
     28:  * "/{entity}/create" creation page of the entity
     29:  *
     30:  * "/{entity}" list page of the entity
     31:  *
     32:  * "/{entity}/{id}" details page of a single entity instance
     33:  *
     34:  * "/{entity}/{id}/edit" edit page of a single entity instance
     35:  *
     36:  * "/{entity}/{id}/delete" POST only deletion route for an entity instance
     37:  *
     38:  * "/{entity}/{id}/{field}/file" renders a file field of an entity instance
     39:  *
     40:  * "/{entity}/{id}/{field}/delete" POST only deletion of a file field of an entity instance
     41:  */
     42: class CRUDControllerProvider implements ControllerProviderInterface {
     43:
     44:     /**
     45:      * Generates the not found page.
     46:      *
     47:      * @param Application $app
     48:      * the Silex application
     49:      * @param string $error
     50:      * the cause of the not found error
     51:      *
     52:      * @return Response
     53:      * the rendered not found page with the status code 404
     54:      */
     55:     protected function getNotFoundPage(Application $app, $error) {
     56:         return new Response($app['twig']->render('@crud/notFound.twig', array(
     57:             'error' => $error,
     58:             'crudEntity' => '',
     59:             'layout' => $app['crud.layout']
     60:         )), 404);
     61:     }
     62:
     63:     /**
     64:      * Delivers the layout for the page in the way it is described in the
     65:      * manual.
     66:      *
     67:      * @param Application $app
     68:      * the Silex application
     69:      * @param string $action
     70:      * the current calling action like "create" or "show"
     71:      * @param string $entity
     72:      * the current calling entity
     73:      *
     74:      * @return string
     75:      * the best fitting layout
     76:      */
     77:     protected function getLayout(Application $app, $action, $entity) {
     78:         if ($app->offsetExists('crud.layout.'.$action.'.'.$entity)) {
     79:             return $app['crud.layout.'.$action.'.'.$entity];
     80:         }
     81:         if ($app->offsetExists('crud.layout.'.$entity)) {
     82:             return $app['crud.layout.'.$entity];
     83:         }
     84:         if ($app->offsetExists('crud.layout.'.$action)) {
     85:             return $app['crud.layout.'.$action];
     86:         }
     87:         return $app['crud.layout'];
     88:     }
     89:
     90:
     91:     /**
     92:      * Implements ControllerProviderInterface::connect() connecting this
     93:      * controller.
     94:      *
     95:      * @param Application $app
     96:      * the Application instance of the Silex application
     97:      *
     98:      * @return SilexController\Collection
     99:      * this method is expected to return the used ControllerCollection instance
    100:      */
    101:     public function connect(Application $app) {
    102:         if ($app->offsetExists('twig.loader.filesystem')) {
    103:             $app['twig.loader.filesystem']->addPath(__DIR__ . '/../views/', 'crud');
    104:         }
    105:
    106:         if (!$app->offsetExists('crud.layout')) {
    107:             $app['crud.layout'] = '@crud/layout.twig';
    108:         }
    109:
    110:         $class = get_class($this);
    111:         $factory = $app['controllers_factory'];
    112:         $factory->get('/resource/static', $class.'::staticFile')
    113:                 ->bind('static');
    114:         $factory->match('/{entity}/create', $class.'::create')
    115:                 ->bind('crudCreate');
    116:         $factory->match('/{entity}', $class.'::showList')
    117:                 ->bind('crudList');
    118:         $factory->match('/{entity}/{id}', $class.'::show')
    119:                 ->bind('crudShow');
    120:         $factory->match('/{entity}/{id}/edit', $class.'::edit')
    121:                 ->bind('crudEdit');
    122:         $factory->post('/{entity}/{id}/delete', $class.'::delete')
    123:                 ->bind('crudDelete');
    124:         $factory->match('/{entity}/{id}/{field}/file', $class.'::renderFile')
    125:                 ->bind('crudRenderFile');
    126:         $factory->post('/{entity}/{id}/{field}/delete', $class.'::deleteFile')
    127:                 ->bind('crudDeleteFile');
    128:         return $factory;
    129:     }
    130:
    131:     /**
    132:      * The controller for the "create" action.
    133:      *
    134:      * @param Application $app
    135:      * the Silex application
    136:      * @param string $entity
    137:      * the current entity
    138:      *
    139:      * @return Response
    140:      * the HTTP response of this action
    141:      */
    142:     public function create(Application $app, $entity) {
    143:         $crudData = $app['crud']->getData($entity);
    144:         if (!$crudData) {
    145:             return $this->getNotFoundPage($app, $app['crud']->translate('entityNotFound'));
    146:         }
    147:
    148:         $errors = array();
    149:         $instance = $crudData->createEmpty();
    150:         $definition = $crudData->getDefinition();
    151:         $fields = $definition->getEditableFieldNames();
    152:
    153:         if ($app['request']->getMethod() == 'POST') {
    154:             foreach ($fields as $field) {
    155:                 if ($definition->getType($field) == 'file') {
    156:                     $file = $app['request']->files->get($field);
    157:                     if ($file) {
    158:                         $instance->set($field, $file->getClientOriginalName());
    159:                     }
    160:                 } else {
    161:                     $instance->set($field, $app['request']->get($field));
    162:                 }
    163:             }
    164:             $validation = $instance->validate($crudData);
    165:             if (!$validation['valid']) {
    166:                 $errors = $validation['errors'];
    167:                 $app['session']->getFlashBag()->add('danger', $app['crud']->translate('create.error'));
    168:             } else {
    169:                 $crudData->create($instance);
    170:                 $id = $instance->get('id');
    171:                 $crudData->createFiles($app['request'], $instance, $entity);
    172:
    173:                 $app['session']->getFlashBag()->add('success', $app['crud']->translate('create.success', array($crudData->getDefinition()->getLabel(), $id)));
    174:                 return $app->redirect($app['url_generator']->generate('crudShow', array('entity' => $entity, 'id' => $id)));
    175:             }
    176:         }
    177:
    178:         $definition = $crudData->getDefinition();
    179:
    180:         return $app['twig']->render('@crud/form.twig', array(
    181:             'crudEntity' => $entity,
    182:             'crudData' => $crudData,
    183:             'entity' => $instance,
    184:             'mode' => 'create',
    185:             'errors' => $errors,
    186:             'layout' => $this->getLayout($app, 'create', $entity)
    187:         ));
    188:     }
    189:
    190:     /**
    191:      * The controller for the "show list" action.
    192:      *
    193:      * @param Application $app
    194:      * the Silex application
    195:      * @param string $entity
    196:      * the current entity
    197:      *
    198:      * @return Response
    199:      * the HTTP response of this action or 404 on invalid input
    200:      */
    201:     public function showList(Application $app, $entity) {
    202:         $crudData = $app['crud']->getData($entity);
    203:         if (!$crudData) {
    204:             return $this->getNotFoundPage($app, $app['crud']->translate('entityNotFound'));
    205:         }
    206:         $entitiesRaw = $crudData->listEntries();
    207:         $entities = array();
    208:         foreach ($entitiesRaw as $curEntity) {
    209:             $crudData->fetchReferences($curEntity);
    210:             $entities[] = $curEntity;
    211:         }
    212:         $definition = $crudData->getDefinition();
    213:         return $app['twig']->render('@crud/list.twig', array(
    214:             'crudEntity' => $entity,
    215:             'definition' => $definition,
    216:             'entities' => $entities,
    217:             'layout' => $this->getLayout($app, 'list', $entity)
    218:         ));
    219:     }
    220:
    221:     /**
    222:      * The controller for the "show" action.
    223:      *
    224:      * @param Application $app
    225:      * the Silex application
    226:      * @param string $entity
    227:      * the current entity
    228:      * @param string $id
    229:      * the instance id to show
    230:      *
    231:      * @return Response
    232:      * the HTTP response of this action or 404 on invalid input
    233:      */
    234:     public function show(Application $app, $entity, $id) {
    235:         $crudData = $app['crud']->getData($entity);
    236:         if (!$crudData) {
    237:             return $this->getNotFoundPage($app, $app['crud']->translate('entityNotFound'));
    238:         }
    239:         $instance = $crudData->get($id);
    240:         $crudData->fetchReferences($instance);
    241:         if (!$instance) {
    242:             return $this->getNotFoundPage($app, $app['crud']->translate('instanceNotFound'));
    243:         }
    244:         $definition = $crudData->getDefinition();
    245:
    246:         $childrenLabelFields = $definition->getChildrenLabelFields();
    247:         $children = array();
    248:         if (count($childrenLabelFields) > 0) {
    249:             foreach ($definition->getChildren() as $child) {
    250:                 $childField = $child[1];
    251:                 $childEntity = $child[2];
    252:                 $childLabelField = key_exists($childEntity, $childrenLabelFields) ? $childrenLabelFields[$childEntity] : 'id';
    253:                 $childCrud = $app['crud']->getData($childEntity);
    254:                 $children[] = array(
    255:                     $childCrud->getDefinition()->getLabel(),
    256:                     $childEntity,
    257:                     $childLabelField,
    258:                     $childCrud->listEntries(array($childField => $instance->get('id')))
    259:                 );
    260:             }
    261:         }
    262:
    263:         return $app['twig']->render('@crud/show.twig', array(
    264:             'crudEntity' => $entity,
    265:             'entity' => $instance,
    266:             'children' => $children,
    267:             'layout' => $this->getLayout($app, 'show', $entity)
    268:         ));
    269:     }
    270:
    271:     /**
    272:      * The controller for the "edit" action.
    273:      *
    274:      * @param Application $app
    275:      * the Silex application
    276:      * @param string $entity
    277:      * the current entity
    278:      * @param string $id
    279:      * the instance id to edit
    280:      *
    281:      * @return Response
    282:      * the HTTP response of this action or 404 on invalid input
    283:      */
    284:     public function edit(Application $app, $entity, $id) {
    285:         $crudData = $app['crud']->getData($entity);
    286:         if (!$crudData) {
    287:             return $this->getNotFoundPage($app, $app['crud']->translate('entityNotFound'));
    288:         }
    289:         $instance = $crudData->get($id);
    290:         if (!$instance) {
    291:             return $this->getNotFoundPage($app, $app['crud']->translate('instanceNotFound'));
    292:         }
    293:
    294:         $definition = $crudData->getDefinition();
    295:
    296:         $errors = array();
    297:         $fields = $definition->getEditableFieldNames();
    298:
    299:
    300:         if ($app['request']->getMethod() == 'POST') {
    301:             foreach ($fields as $field) {
    302:                 if ($definition->getType($field) == 'file') {
    303:                     $file = $app['request']->files->get($field);
    304:                     if ($file) {
    305:                         $instance->set($field, $file->getClientOriginalName());
    306:                     }
    307:                 } else {
    308:                     $instance->set($field, $app['request']->get($field));
    309:                 }
    310:             }
    311:             $validation = $instance->validate($crudData);
    312:             if (!$validation['valid']) {
    313:                 $app['session']->getFlashBag()->add('danger', $app['crud']->translate('edit.error'));
    314:                 $errors = $validation['errors'];
    315:             } else {
    316:                 $crudData->update($instance);
    317:                 $crudData->updateFiles($app['request'], $instance, $entity);
    318:                 $app['session']->getFlashBag()->add('success', $app['crud']->translate('edit.success', array($crudData->getDefinition()->getLabel(), $id)));
    319:                 return $app->redirect($app['url_generator']->generate('crudShow', array('entity' => $entity, 'id' => $id)));
    320:             }
    321:         }
    322:
    323:         return $app['twig']->render('@crud/form.twig', array(
    324:             'crudEntity' => $entity,
    325:             'crudData' => $crudData,
    326:             'entity' => $instance,
    327:             'mode' => 'edit',
    328:             'errors' => $errors,
    329:             'layout' => $this->getLayout($app, 'edit', $entity)
    330:         ));
    331:     }
    332:
    333:     /**
    334:      * The controller for the "delete" action.
    335:      *
    336:      * @param Application $app
    337:      * the Silex application
    338:      * @param string $entity
    339:      * the current entity
    340:      * @param string $id
    341:      * the instance id to delete
    342:      *
    343:      * @return Response
    344:      * redirects to the entity list page or 404 on invalid input
    345:      */
    346:     public function delete(Application $app, $entity, $id) {
    347:         $crudData = $app['crud']->getData($entity);
    348:         if (!$crudData) {
    349:             return $this->getNotFoundPage($app, $app['crud']->translate('entityNotFound'));
    350:         }
    351:         $instance = $crudData->get($id);
    352:         if (!$instance) {
    353:             return $this->getNotFoundPage($app, $app['crud']->translate('instanceNotFound'));
    354:         }
    355:
    356:         $crudData->deleteFiles($instance, $entity);
    357:         $deleted = $crudData->delete($id);
    358:         if ($deleted) {
    359:             $app['session']->getFlashBag()->add('success', $app['crud']->translate('delete.success', array($crudData->getDefinition()->getLabel())));
    360:             return $app->redirect($app['url_generator']->generate('crudList', array('entity' => $entity)));
    361:         } else {
    362:             $app['session']->getFlashBag()->add('danger', $app['crud']->translate('delete.error', array($crudData->getDefinition()->getLabel())));
    363:             return $app->redirect($app['url_generator']->generate('crudShow', array('entity' => $entity, 'id' => $id)));
    364:         }
    365:     }
    366:
    367:     /**
    368:      * The controller for the "render file" action.
    369:      *
    370:      * @param Application $app
    371:      * the Silex application
    372:      * @param string $entity
    373:      * the current entity
    374:      * @param string $id
    375:      * the instance id
    376:      * @param string $field
    377:      * the field of the file to render of the instance
    378:      *
    379:      * @return Response
    380:      * the rendered file
    381:      */
    382:     public function renderFile(Application $app, $entity, $id, $field) {
    383:         $crudData = $app['crud']->getData($entity);
    384:         if (!$crudData) {
    385:             return $this->getNotFoundPage($app, $app['crud']->translate('entityNotFound'));
    386:         }
    387:         $instance = $crudData->get($id);
    388:         $definition = $crudData->getDefinition();
    389:         if (!$instance) {
    390:             return $this->getNotFoundPage($app, $app['crud']->translate('instanceNotFound'));
    391:         }
    392:         if ($definition->getType($field) != 'file' || !$instance->get($field)) {
    393:             return $this->getNotFoundPage($app, $app['crud']->translate('instanceNotFound'));
    394:         }
    395:         return $crudData->renderFile($instance, $entity, $field);
    396:     }
    397:
    398:     /**
    399:      * The controller for the "delete file" action.
    400:      *
    401:      * @param Application $app
    402:      * the Silex application
    403:      * @param string $entity
    404:      * the current entity
    405:      * @param string $id
    406:      * the instance id
    407:      * @param string $field
    408:      * the field of the file to delete of the instance
    409:      *
    410:      * @return Response
    411:      * redirects to the instance details page or 404 on invalid input
    412:      */
    413:     public function deleteFile(Application $app, $entity, $id, $field) {
    414:         $crudData = $app['crud']->getData($entity);
    415:         if (!$crudData) {
    416:             return $this->getNotFoundPage($app, $app['crud']->translate('entityNotFound'));
    417:         }
    418:         $instance = $crudData->get($id);
    419:         if (!$instance) {
    420:             return $this->getNotFoundPage($app, $app['crud']->translate('instanceNotFound'));
    421:         }
    422:         if (!$crudData->getDefinition()->isRequired($field)) {
    423:             $crudData->deleteFile($instance, $entity, $field);
    424:             $instance->set($field, '');
    425:             $crudData->update($instance);
    426:             $app['session']->getFlashBag()->add('success', $app['crud']->translate('file.deleted'));
    427:         } else {
    428:             $app['session']->getFlashBag()->add('danger', $app['crud']->translate('file.notdeleted'));
    429:         }
    430:         return $app->redirect($app['url_generator']->generate('crudShow', array('entity' => $entity, 'id' => $id)));
    431:     }
    432:
    433:     /**
    434:      * The controller for serving static files.
    435:      *
    436:      * @param Application $app
    437:      * the Silex application
    438:      *
    439:      * @return Response
    440:      * redirects to the instance details page or 404 on invalid input
    441:      */
    442:     public function staticFile(Application $app) {
    443:         $fileParam = $app['request']->get('file');
    444:         if (!$fileParam) {
    445:             return $this->getNotFoundPage($app, $app['crud']->translate('resourceNotFound'));
    446:         }
    447:
    448:         $file = __DIR__.'/../static/'.$fileParam;
    449:         if (!file_exists($file)) {
    450:             return $this->getNotFoundPage($app, $app['crud']->translate('resourceNotFound'));
    451:         }
    452:
    453:         $extension = pathinfo($file, PATHINFO_EXTENSION);
    454:         $mimeType = '';
    455:         if (strtolower($extension) === 'css') {
    456:             $mimeType = 'text/css';
    457:         } else {
    458:             $finfo = finfo_open(FILEINFO_MIME_TYPE);
    459:             $mimeType = finfo_file($finfo, $file);
    460:             finfo_close($finfo);
    461:         }
    462:
    463:         $size = filesize($file);
    464:
    465:         $response = new StreamedResponse(function () use ($file) {
    466:             set_time_limit(0);
    467:             $handle = fopen($file,"rb");
    468:             if ($handle !== false) {
    469:                 $chunkSize = 1024 * 1024;
    470:                 while (!feof($handle)) {
    471:                     $buffer = fread($handle, $chunkSize);
    472:                     echo $buffer;
    473:                     flush();
    474:                 }
    475:                 fclose($handle);
    476:             }
    477:         }, 200, array(
    478:             'Content-Type' => $mimeType,
    479:             'Content-Disposition' => 'attachment; filename="'.basename($file).'"',
    480:             'Content-length' => $size
    481:         ));
    482:         $response->send();
    483:
    484:         return $response;
    485:     }
    486: }
    487: