├── var
├── cache
│ └── .gitkeep
├── logs
│ └── .gitkeep
└── sessions
│ └── .gitkeep
├── web
├── favicon.ico
├── apple-touch-icon.png
├── robots.txt
├── app.php
├── app_dev.php
├── app_acceptance.php
└── .htaccess
├── features
├── Dummy
│ └── Image
│ │ ├── pk140.jpg
│ │ └── phit200x100.png
└── bootstrap
│ └── FeatureContext.php
├── app
├── AppCache.php
├── serializer
│ └── FOSUB
│ │ └── Model.User.yml
├── .htaccess
├── config
│ ├── routing.yml
│ ├── config_test.yml
│ ├── routing_dev.yml
│ ├── routing_api.yml
│ ├── config_acceptance.yml
│ ├── config_prod.yml
│ ├── config_dev.yml
│ ├── parameters.yml.dist
│ ├── security.yml
│ ├── config.yml
│ └── services.yml
├── autoload.php
├── Resources
│ └── views
│ │ ├── base.html.twig
│ │ └── default
│ │ └── index.html.twig
└── AppKernel.php
├── src
├── .htaccess
└── AppBundle
│ ├── AppBundle.php
│ ├── DTO
│ ├── SymfonyFormDTO.php
│ ├── DTOInterface.php
│ ├── DTOConvertableInterface.php
│ ├── DTOUpdatableInterface.php
│ ├── AccountDTO.php
│ └── FileDTO.php
│ ├── Form
│ ├── Type
│ │ ├── UserType.php
│ │ ├── FileType.php
│ │ └── AccountType.php
│ ├── Handler
│ │ ├── FormHandlerInterface.php
│ │ └── FormHandler.php
│ └── Transformer
│ │ ├── ManyEntityToIdObjectTransformer.php
│ │ └── EntityToIdObjectTransformer.php
│ ├── Util
│ ├── FilesystemInterface.php
│ └── UploadFilesystem.php
│ ├── Repository
│ ├── RepositoryInterface.php
│ ├── UserRepositoryInterface.php
│ ├── Restricted
│ │ ├── RestrictedRepository.php
│ │ ├── RestrictedUserRepository.php
│ │ ├── RestrictedAccountRepository.php
│ │ └── RestrictedFileRepository.php
│ ├── AccountRepositoryInterface.php
│ ├── FileRepositoryInterface.php
│ └── Doctrine
│ │ ├── CommonDoctrineRepository.php
│ │ ├── DoctrineUserRepository.php
│ │ ├── DoctrineAccountRepository.php
│ │ └── DoctrineFileRepository.php
│ ├── Security
│ └── Authorization
│ │ ├── UserOwnableInterface.php
│ │ └── Voter
│ │ ├── UserVoter.php
│ │ ├── FileVoter.php
│ │ └── AccountVoter.php
│ ├── Exception
│ ├── HttpContentTypeException.php
│ └── InvalidFormException.php
│ ├── Controller
│ ├── RestLoginController.php
│ ├── UsersController.php
│ ├── AccountsController.php
│ └── FilesController.php
│ ├── Factory
│ ├── AccountFactoryInterface.php
│ ├── FileFactoryInterface.php
│ ├── AccountFactory.php
│ └── FileFactory.php
│ ├── Entity
│ ├── Repository
│ │ ├── UserEntityRepository.php
│ │ ├── AccountEntityRepository.php
│ │ └── FileEntityRepository.php
│ ├── User.php
│ ├── Account.php
│ └── File.php
│ ├── Handler
│ ├── FileHandlerInterface.php
│ ├── HandlerInterface.php
│ ├── UserHandler.php
│ ├── AccountHandler.php
│ └── FileHandler.php
│ ├── Model
│ ├── UploadedFileInterface.php
│ ├── UserInterface.php
│ ├── FileInterface.php
│ ├── AccountInterface.php
│ └── UploadedFile.php
│ ├── DataTransformer
│ ├── FileDataTransformer.php
│ └── AccountDataTransformer.php
│ ├── Features
│ ├── developer_experience.feature
│ ├── Context
│ │ ├── FileStorageContext.php
│ │ ├── MysqlDatabaseContext.php
│ │ ├── UserSetupContext.php
│ │ ├── FileSetupContext.php
│ │ └── AccountSetupContext.php
│ ├── user.feature
│ └── file.feature
│ └── Event
│ └── Listener
│ └── ContentTypeListener.php
├── .gitignore
├── spec
└── AppBundle
│ ├── Entity
│ └── UserSpec.php
│ ├── Form
│ ├── Type
│ │ └── UserTypeSpec.php
│ └── Handler
│ │ └── FormHandlerSpec.php
│ ├── Exception
│ ├── InvalidFormExceptionSpec.php
│ └── HttpContentTypeExceptionSpec.php
│ ├── Repository
│ ├── Doctrine
│ │ └── UserRepositorySpec.php
│ └── Restricted
│ │ └── UserRepositorySpec.php
│ ├── Security
│ └── Authorisation
│ │ └── Voter
│ │ └── UserVoterSpec.php
│ ├── Handler
│ └── UserHandlerSpec.php
│ └── Event
│ └── Listener
│ └── ContentTypeListenerSpec.php
├── tests
└── AppBundle
│ └── Controller
│ └── DefaultControllerTest.php
├── bin
├── console
└── symfony_requirements
├── phpunit.xml.dist
├── behat.yml
├── composer.json
└── README.md
/var/cache/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/var/logs/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/var/sessions/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codereviewvideos/symfony-3-rest-api-example/HEAD/web/favicon.ico
--------------------------------------------------------------------------------
/web/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codereviewvideos/symfony-3-rest-api-example/HEAD/web/apple-touch-icon.png
--------------------------------------------------------------------------------
/web/robots.txt:
--------------------------------------------------------------------------------
1 | # www.robotstxt.org/
2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
3 |
4 | User-agent: *
5 |
--------------------------------------------------------------------------------
/features/Dummy/Image/pk140.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codereviewvideos/symfony-3-rest-api-example/HEAD/features/Dummy/Image/pk140.jpg
--------------------------------------------------------------------------------
/app/AppCache.php:
--------------------------------------------------------------------------------
1 |
2 | Require all denied
3 |
4 |
5 | Order deny,allow
6 | Deny from all
7 |
8 |
--------------------------------------------------------------------------------
/src/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Require all denied
3 |
4 |
5 | Order deny,allow
6 | Deny from all
7 |
8 |
--------------------------------------------------------------------------------
/src/AppBundle/AppBundle.php:
--------------------------------------------------------------------------------
1 | shouldHaveType('AppBundle\Entity\User');
13 | $this->shouldImplement('AppBundle\Model\UserInterface');
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/AppBundle/Repository/RepositoryInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}Welcome!{% endblock %}
6 | {% block stylesheets %}{% endblock %}
7 |
8 |
9 |
10 | {% block body %}{% endblock %}
11 | {% block javascripts %}{% endblock %}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/AppBundle/Exception/HttpContentTypeException.php:
--------------------------------------------------------------------------------
1 | beConstructedWith(['Some\Form\Class']);
13 | }
14 |
15 | function it_is_initializable()
16 | {
17 | $this->shouldHaveType('AppBundle\Form\Type\UserType');
18 | $this->shouldHaveType('FOS\UserBundle\Form\Type\ProfileFormType');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/AppBundle/Entity/Repository/UserEntityRepository.php:
--------------------------------------------------------------------------------
1 | true])
14 | {
15 | $this->_em->persist($user);
16 |
17 | if ($options['flush'] === true) {
18 | $this->_em->flush();
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/src/AppBundle/Handler/FileHandlerInterface.php:
--------------------------------------------------------------------------------
1 | request('GET', '/');
14 |
15 | $this->assertEquals(200, $client->getResponse()->getStatusCode());
16 | $this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text());
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/AppBundle/Repository/UserRepositoryInterface.php:
--------------------------------------------------------------------------------
1 | true]);
24 | }
--------------------------------------------------------------------------------
/app/config/config_acceptance.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: config_test.yml }
3 |
4 | framework:
5 | profiler:
6 | only_exceptions: false
7 | collect: true
8 |
9 | web_profiler:
10 | toolbar: true
11 |
12 | doctrine:
13 | dbal:
14 | dbname: "%database_name%_acceptance"
15 |
16 | csa_guzzle:
17 | logger: true
18 | clients:
19 | local_test_api:
20 | config:
21 | base_url: http://symfony-rest-example.dev/app_acceptance.php/
22 |
23 | # Oneup Flysystem
24 | oneup_flysystem:
25 | adapters:
26 | local_adapter:
27 | local:
28 | directory: %kernel.root_dir%/../features/uploadTemp
--------------------------------------------------------------------------------
/src/AppBundle/Exception/InvalidFormException.php:
--------------------------------------------------------------------------------
1 | form = $form;
20 | }
21 |
22 | /**
23 | * @return array|null
24 | */
25 | public function getForm()
26 | {
27 | return $this->form;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/AppBundle/Model/UploadedFileInterface.php:
--------------------------------------------------------------------------------
1 | shouldHaveType('AppBundle\Exception\InvalidFormException');
14 | }
15 |
16 | function it_should_have_a_sensible_default_error_message()
17 | {
18 | $this->getMessage()->shouldReturn(InvalidFormException::DEFAULT_ERROR_MESSAGE);
19 | }
20 |
21 | function it_should_be_able_to_return_the_underlying_form()
22 | {
23 | $this->beConstructedWith(['a']);
24 |
25 | $this->getForm()->shouldReturn(['a']);
26 | }
27 | }
--------------------------------------------------------------------------------
/spec/AppBundle/Exception/HttpContentTypeExceptionSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType('AppBundle\Exception\HttpContentTypeException');
14 | $this->shouldHaveType('Symfony\Component\HttpKernel\Exception\HttpException');
15 | }
16 |
17 | function it_should_have_a_sensible_default_error_message()
18 | {
19 | $this->getMessage()->shouldReturn(HttpContentTypeException::ERROR_MESSAGE);
20 | }
21 |
22 | function it_should_have_the_correct_error_code()
23 | {
24 | $this->getStatusCode()->shouldReturn(HttpContentTypeException::ERROR_CODE);
25 | }
26 | }
--------------------------------------------------------------------------------
/src/AppBundle/Entity/Repository/AccountEntityRepository.php:
--------------------------------------------------------------------------------
1 | getEntityManager()
18 | ->createQueryBuilder()
19 | ->select('a')
20 | ->from('AppBundle\Entity\Account', 'a')
21 | ->join('a.users', 'u')
22 | ->where('u.id = :userId')
23 | ->setParameter('userId', $user->getId())
24 | ->getQuery();
25 |
26 | return $query->getResult();
27 | }
28 | }
--------------------------------------------------------------------------------
/src/AppBundle/Factory/AccountFactory.php:
--------------------------------------------------------------------------------
1 | getName());
26 |
27 | foreach ($accountDTO->getUsers() as $user) { /** @var $user \AppBundle\Model\UserInterface */
28 | $user->addAccount($account);
29 | }
30 |
31 | return $account;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/AppBundle/DataTransformer/FileDataTransformer.php:
--------------------------------------------------------------------------------
1 | setName($file->getDisplayedFileName());
19 |
20 | return $dto;
21 | }
22 |
23 | /**
24 | * @param FileInterface $file
25 | * @param FileDTO $dto
26 | * @return FileInterface
27 | */
28 | public function updateFromDTO(FileInterface $file, FileDTO $dto)
29 | {
30 | if ($file->getDisplayedFileName() !== $dto->getName()) {
31 | $file->changeDisplayedFileName($dto->getName());
32 | }
33 |
34 | return $file;
35 | }
36 | }
--------------------------------------------------------------------------------
/src/AppBundle/Model/UserInterface.php:
--------------------------------------------------------------------------------
1 | setName($account->getName());
16 | $dto->setUsers($account->getUsers());
17 |
18 | return $dto;
19 | }
20 |
21 | public function updateFromDTO(AccountInterface $account, AccountDTO $dto)
22 | {
23 | if ($account->getName() !== $dto->getName()) {
24 | $account->changeName($dto->getName());
25 | }
26 |
27 | $account->removeAllUsers();
28 |
29 | foreach ($dto->getUsers() as $user) { /** @var UserInterface $user */
30 | $user->addAccount($account);
31 | }
32 |
33 | return $account;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev');
21 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod';
22 |
23 | if ($debug) {
24 | Debug::enable();
25 | }
26 |
27 | $kernel = new AppKernel($env, $debug);
28 | $application = new Application($kernel);
29 | $application->run($input);
30 |
--------------------------------------------------------------------------------
/web/app.php:
--------------------------------------------------------------------------------
1 | unregister();
18 | $apcLoader->register(true);
19 | */
20 |
21 | $kernel = new AppKernel('prod', false);
22 | $kernel->loadClassCache();
23 | //$kernel = new AppCache($kernel);
24 |
25 | // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter
26 | //Request::enableHttpMethodParameterOverride();
27 | $request = Request::createFromGlobals();
28 | $response = $kernel->handle($request);
29 | $response->send();
30 | $kernel->terminate($request, $response);
31 |
--------------------------------------------------------------------------------
/src/AppBundle/Features/developer_experience.feature:
--------------------------------------------------------------------------------
1 | Feature: To improve the developer experience of our API
2 |
3 | In order to offer an API user a better experience
4 | As an API developer
5 | I need to return useful information when situations may be otherwise confusing
6 |
7 |
8 | Background:
9 | Given there are Users with the following details:
10 | | uid | username | email | password |
11 | | u1 | peter | peter@test.com | testpass |
12 | | u2 | john | john@test.org | johnpass |
13 | And I am successfully logged in with username: "peter", and password: "testpass"
14 | And when consuming the endpoint I use the "headers/content-type" of "application/json"
15 |
16 |
17 | Scenario: User must have the right Content-type
18 | When I have forgotten to set the "headers/content-type"
19 | And I send a "PATCH" request to "/users/u1"
20 | Then the response code should be 415
21 | And the response should contain json:
22 | """
23 | {
24 | "code": 415,
25 | "message": "Invalid or missing Content-type header"
26 | }
27 | """
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | tests
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | src
27 |
28 | src/*Bundle/Resources
29 | src/*/*Bundle/Resources
30 | src/*/Bundle/*Bundle/Resources
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/AppBundle/Repository/Restricted/RestrictedRepository.php:
--------------------------------------------------------------------------------
1 | authorizationChecker->isGranted($attributes, $object)) {
28 | throw new AccessDeniedHttpException($message);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/AppBundle/Repository/AccountRepositoryInterface.php:
--------------------------------------------------------------------------------
1 | filesystem = $filesystem;
25 | }
26 |
27 | /**
28 | * @Then the file with internal name: :internalName should have been deleted
29 | */
30 | public function theFileWithInternalNameShouldHaveBeenDeleted($internalName)
31 | {
32 | $this->filesystem->assertAbsent($internalName);
33 | }
34 |
35 | /**
36 | * @Then the file with internal name: :internalName should not have been deleted
37 | */
38 | public function theFileWithInternalNameShouldNotHaveBeenDeleted($internalName)
39 | {
40 | $this->filesystem->assertPresent($internalName);
41 | }
42 | }
--------------------------------------------------------------------------------
/src/AppBundle/Form/Type/FileType.php:
--------------------------------------------------------------------------------
1 | add('name', TextType::class, [
21 | 'required' => false,
22 | ])
23 | ;
24 |
25 | if ($options['has_file']) {
26 | $builder->add('uploadedFile', CoreFileType::class, [
27 | 'multiple' => false,
28 | ]);
29 | }
30 | }
31 |
32 | public function configureOptions(OptionsResolver $resolver)
33 | {
34 | $resolver->setDefaults([
35 | 'data_class' => 'AppBundle\DTO\FileDTO',
36 | 'has_file' => true,
37 | ]);
38 | }
39 |
40 | public function getName()
41 | {
42 | return 'file';
43 | }
44 | }
--------------------------------------------------------------------------------
/src/AppBundle/Factory/FileFactory.php:
--------------------------------------------------------------------------------
1 | create(
36 | $uploadedFile->getOriginalFileName(),
37 | $internalFileName,
38 | $uploadedFile->getFileExtension(),
39 | $uploadedFile->getFileSize()
40 | );
41 | }
42 | }
--------------------------------------------------------------------------------
/features/bootstrap/FeatureContext.php:
--------------------------------------------------------------------------------
1 | doctrine = $doctrine;
28 | $this->manager = $doctrine->getManager();
29 |
30 | $this->schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->manager);
31 |
32 | $this->classes = $this->manager->getMetadataFactory()->getAllMetadata();
33 | }
34 |
35 | /**
36 | * @BeforeScenario
37 | */
38 | public function createSchema()
39 | {
40 | $this->schemaTool->dropSchema($this->classes);
41 | $this->schemaTool->createSchema($this->classes);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/web/app_dev.php:
--------------------------------------------------------------------------------
1 | loadClassCache();
29 | $request = Request::createFromGlobals();
30 | $response = $kernel->handle($request);
31 | $response->send();
32 | $kernel->terminate($request, $response);
33 |
--------------------------------------------------------------------------------
/web/app_acceptance.php:
--------------------------------------------------------------------------------
1 | loadClassCache();
29 | $request = Request::createFromGlobals();
30 | $response = $kernel->handle($request);
31 | $response->send();
32 | $kernel->terminate($request, $response);
33 |
--------------------------------------------------------------------------------
/src/AppBundle/Handler/HandlerInterface.php:
--------------------------------------------------------------------------------
1 | getRequest();
17 |
18 | if ($request->headers->contains('Content-type', self::MIME_TYPE_APPLICATION_JSON)) {
19 | return true;
20 | }
21 |
22 | if ($request->getMethod() === Request::METHOD_GET) {
23 | return true;
24 | }
25 |
26 | if ($this->isMultipartFilePost($request)) {
27 | return true;
28 | }
29 |
30 | throw new HttpContentTypeException();
31 | }
32 |
33 | private function isMultipartFilePost(Request $request)
34 | {
35 | $contentType = $request->headers->get('Content-type');
36 |
37 | $isMultipart = (strpos($contentType, self::MIME_TYPE_MULTIPART_FORM_DATA) !== false ? true : false);
38 |
39 | if ($isMultipart && $request->get('_route') === 'post_accounts_files') {
40 | return true;
41 | }
42 |
43 | return false;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/spec/AppBundle/Repository/Doctrine/UserRepositorySpec.php:
--------------------------------------------------------------------------------
1 | entityRepository = $entityRepository;
17 |
18 | $this->beConstructedWith($entityRepository);
19 | }
20 |
21 | function it_is_initializable()
22 | {
23 | $this->shouldHaveType('AppBundle\Repository\Doctrine\UserRepository');
24 | $this->shouldImplement('AppBundle\Repository\UserRepositoryInterface');
25 | }
26 |
27 | function it_can_find()
28 | {
29 | $id = 24234;
30 | $this->find($id);
31 | $this->entityRepository->find($id)->shouldHaveBeenCalled();
32 | }
33 |
34 | function it_can_save_a_user_to_the_repository(UserInterface $user)
35 | {
36 | $this->save($user);
37 | $this->entityRepository->save($user, ['flush'=>true])->shouldHaveBeenCalled();
38 | }
39 |
40 | function it_can_save_without_flushing(UserInterface $user)
41 | {
42 | $this->save($user, ['flush'=>false]);
43 | $this->entityRepository->save($user, ['flush'=>false])->shouldHaveBeenCalled();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/behat.yml:
--------------------------------------------------------------------------------
1 | default:
2 |
3 | suites:
4 | default:
5 | type: symfony_bundle
6 | bundle: AppBundle
7 | contexts:
8 | - FeatureContext:
9 | doctrine: "@doctrine"
10 | - AppBundle\Features\Context\RestApiContext:
11 | client: "@csa_guzzle.client.local_test_api"
12 | dummyDataPath: "%paths.base%/features/Dummy/"
13 | - AppBundle\Features\Context\AccountSetupContext:
14 | userManager: "@fos_user.user_manager"
15 | accountFactory: "@crv.factory.account_factory"
16 | em: "@doctrine.orm.entity_manager"
17 | - AppBundle\Features\Context\FileSetupContext:
18 | userManager: "@fos_user.user_manager"
19 | fileFactory: "@crv.factory.file_factory"
20 | em: "@doctrine.orm.entity_manager"
21 | filesystem: "@oneup_flysystem.local_filesystem"
22 | dummyDataPath: "%paths.base%/features/Dummy/"
23 | - AppBundle\Features\Context\FileStorageContext:
24 | filesystem: "@oneup_flysystem.local_filesystem"
25 | - AppBundle\Features\Context\MysqlDatabaseContext:
26 | em: "@doctrine.orm.entity_manager"
27 | - AppBundle\Features\Context\UserSetupContext:
28 | userManager: "@fos_user.user_manager"
29 | em: "@doctrine.orm.entity_manager"
30 |
31 |
32 | extensions:
33 | Behat\Symfony2Extension:
34 | kernel:
35 | env: "acceptance"
36 | debug: "true"
37 |
38 |
--------------------------------------------------------------------------------
/src/AppBundle/Repository/Restricted/RestrictedUserRepository.php:
--------------------------------------------------------------------------------
1 | repository = $repository;
23 | $this->authorizationChecker = $authorizationChecker;
24 | }
25 |
26 | public function save(UserInterface $user, array $arguments = [])
27 | {
28 | $this->authorizationChecker->isGranted('view', $user);
29 |
30 | $this->repository->save($user);
31 | }
32 |
33 | public function delete(UserInterface $user, array $arguments = [])
34 | {
35 | $this->authorizationChecker->isGranted('view', $user);
36 |
37 | $this->repository->delete($user);
38 | }
39 |
40 | public function findOneById($id)
41 | {
42 | $user = $this->repository->findOneById($id);
43 |
44 | $this->denyAccessUnlessGranted('view', $user);
45 |
46 | return $user;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/app/config/parameters.yml.dist:
--------------------------------------------------------------------------------
1 | # This file is a "template" of what your parameters.yml file should look like
2 | # Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production.
3 | # http://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
4 | parameters:
5 | database_host: 127.0.0.1
6 | database_port: ~
7 | database_name: symfony
8 | database_user: root
9 | database_password: ~
10 | # You should uncomment this if you want use pdo_sqlite
11 | # database_path: "%kernel.root_dir%/data.db3"
12 |
13 | mailer_transport: smtp
14 | mailer_host: 127.0.0.1
15 | mailer_user: ~
16 | mailer_password: ~
17 |
18 | # A secret key that's used to generate certain security-related tokens
19 | secret: ThisTokenIsNotSoSecretChangeIt
20 |
21 | # fos user bundle
22 | fos_user.from_email.address: "noreply@example.com"
23 | fos_user.from_email.sender_name: "Demo App"
24 |
25 | # nelmio cors
26 | cors_allow_origin: 'http://localhost'
27 |
28 | # nelmio api docs
29 | api_name: 'Your API name'
30 | api_description: 'The full description of your API'
31 |
32 | # lexik jwt
33 | jwt_private_key_path: %kernel.root_dir%/../var/jwt/private.pem # ssh private key path
34 | jwt_public_key_path: %kernel.root_dir%/../var/jwt/public.pem # ssh public key path
35 | jwt_key_pass_phrase: '1234' # ssh key pass phrase
36 | jwt_token_ttl: 86400
--------------------------------------------------------------------------------
/src/AppBundle/Entity/Repository/FileEntityRepository.php:
--------------------------------------------------------------------------------
1 | getEntityManager()
19 | ->createQueryBuilder()
20 | ->select('f')
21 | ->from('AppBundle\Entity\File', 'f')
22 | ->join('f.accounts', 'a')
23 | ->where('a.id = :accountId')
24 | ->setParameter('accountId', $account->getId())
25 | ->getQuery();
26 |
27 | return new ArrayCollection(
28 | $query->getResult()
29 | );
30 | }
31 |
32 | /**
33 | * @param UserInterface $user
34 | * @return array
35 | */
36 | public function findAllForUser(UserInterface $user)
37 | {
38 | $query = $this->getEntityManager()
39 | ->createQueryBuilder()
40 | ->select('f')
41 | ->from('AppBundle\Entity\File', 'f')
42 | ->join('f.accounts', 'a')
43 | ->join('a.users', 'u')
44 | ->where('u.id = :userId')
45 | ->setParameter('userId', $user->getId())
46 | ->getQuery();
47 |
48 | return new ArrayCollection(
49 | $query->getResult()
50 | );
51 | }
52 | }
--------------------------------------------------------------------------------
/src/AppBundle/Model/FileInterface.php:
--------------------------------------------------------------------------------
1 | em = $em;
25 | }
26 |
27 | /**
28 | * @return EntityManagerInterface
29 | */
30 | public function getEntityManager()
31 | {
32 | return $this->em;
33 | }
34 |
35 | /**
36 | * @param mixed $object
37 | */
38 | public function refresh($object)
39 | {
40 | $this->em->refresh($object);
41 | }
42 |
43 | /**
44 | * @param mixed $object
45 | * @param array $arguments
46 | */
47 | public function save($object, array $arguments = ['flush'=>true])
48 | {
49 | $this->em->persist($object);
50 |
51 | if ($arguments['flush'] === true) {
52 | $this->em->flush();
53 | }
54 | }
55 |
56 | /**
57 | * @param mixed $object
58 | * @param array $arguments
59 | */
60 | public function delete($object, array $arguments = ['flush'=>true])
61 | {
62 | $this->em->remove($object);
63 |
64 | if ($arguments['flush'] === true) {
65 | $this->em->flush();
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/AppBundle/Form/Handler/FormHandler.php:
--------------------------------------------------------------------------------
1 | formFactory = $formFactory;
32 | $this->formType = $formType;
33 | }
34 |
35 | /**
36 | * @param mixed $object
37 | * @param array $parameters
38 | * @param string $method
39 | * @param array $options
40 | * @return mixed
41 | * @throws InvalidFormException
42 | */
43 | public function handle($object, array $parameters, $method, array $options = [])
44 | {
45 | $options = array_replace_recursive([
46 | 'method' => $method,
47 | 'csrf_protection' => false,
48 | ], $options);
49 |
50 | $form = $this->formFactory->create(get_class($this->formType), $object, $options);
51 |
52 | $form->submit($parameters, 'PATCH' !== $method);
53 |
54 | if (!$form->isValid()) {
55 | throw new InvalidFormException($form);
56 | }
57 |
58 | return $form->getData();
59 | }
60 | }
--------------------------------------------------------------------------------
/src/AppBundle/Security/Authorization/Voter/UserVoter.php:
--------------------------------------------------------------------------------
1 | supportsClass(get_class($requestedUser))) {
30 | return VoterInterface::ACCESS_ABSTAIN;
31 | }
32 |
33 | // set the attribute to check against
34 | $attribute = $attributes[0];
35 |
36 | // check if the given attribute is covered by this voter
37 | if (!$this->supportsAttribute($attribute)) {
38 | return VoterInterface::ACCESS_ABSTAIN;
39 | }
40 |
41 | // get current logged in user
42 | $loggedInUser = $token->getUser();
43 |
44 | // make sure there is a user object (i.e. that the user is logged in)
45 | if ($loggedInUser === $requestedUser) {
46 | return VoterInterface::ACCESS_GRANTED;
47 | }
48 |
49 | return VoterInterface::ACCESS_DENIED;
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/spec/AppBundle/Form/Handler/FormHandlerSpec.php:
--------------------------------------------------------------------------------
1 | form = $form;
25 |
26 | $formFactory->create(Argument::any(), Argument::any(), Argument::any())->willReturn($this->form);
27 |
28 | $this->beConstructedWith($formFactory, $formType);
29 | }
30 |
31 | function it_is_initializable()
32 | {
33 | $this->shouldHaveType('AppBundle\Form\Handler\FormHandler');
34 | $this->shouldImplement('AppBundle\Form\Handler\FormHandlerInterface');
35 | }
36 |
37 | function it_should_throw_if_form_is_invalid()
38 | {
39 | $this->form->submit(Argument::any(), Argument::any())->willReturn(false);
40 | $this->form->isValid()->willReturn(false);
41 |
42 | $this
43 | ->shouldThrow('AppBundle\Exception\InvalidFormException')
44 | ->during('handle', [new \StdClass(), [1,2,3], 'BAD', []])
45 | ;
46 | }
47 |
48 | function it_can_handle_a_form()
49 | {
50 | $this->form->submit(Argument::any(), Argument::any())->shouldBeCalled();
51 | $this->form->isValid()->willReturn(true);
52 | $this->form->getData()->shouldBeCalled();
53 |
54 | $this->handle(new \StdClass(), [1,2,3], 'FAKE', []);
55 | }
56 | }
--------------------------------------------------------------------------------
/src/AppBundle/Form/Transformer/ManyEntityToIdObjectTransformer.php:
--------------------------------------------------------------------------------
1 | entityToIdObjectTransformer = $entityToIdObjectTransformer;
21 | }
22 |
23 | /**
24 | * Do nothing
25 | *
26 | * @param array $array
27 | * @return array
28 | */
29 | public function transform($array)
30 | {
31 | $transformed = [];
32 |
33 | if (empty($array) || null === $array) {
34 | return $transformed;
35 | }
36 |
37 | foreach ($array as $k => $v) {
38 | $transformed[] = $this->entityToIdObjectTransformer->transform($v);
39 | }
40 |
41 | return $transformed;
42 | }
43 |
44 | /**
45 | * Transforms an array of arrays including an identifier to an object.
46 | *
47 | * @param array $array
48 | *
49 | * @return array
50 | */
51 | public function reverseTransform($array)
52 | {
53 | if (!is_array($array)) {
54 | $array = [$array];
55 | }
56 |
57 | $reverseTransformed = [];
58 |
59 | foreach ($array as $k => $v) {
60 | $reverseTransformed[] = $this->entityToIdObjectTransformer->reverseTransform($v);
61 | }
62 |
63 | return $reverseTransformed;
64 | }
65 | }
--------------------------------------------------------------------------------
/src/AppBundle/Repository/Doctrine/DoctrineUserRepository.php:
--------------------------------------------------------------------------------
1 | commonRepository = $commonRepository;
26 | $this->em = $em;
27 | }
28 |
29 | /**
30 | * @param UserInterface $user
31 | */
32 | public function refresh(UserInterface $user)
33 | {
34 | $this->commonRepository->refresh($user);
35 | }
36 |
37 | /**
38 | * @param UserInterface $user
39 | * @param array $arguments
40 | */
41 | public function save(UserInterface $user, array $arguments = ['flush'=>true])
42 | {
43 | $this->commonRepository->save($user, $arguments);
44 | }
45 |
46 | /**
47 | * @param UserInterface $user
48 | * @param array $arguments
49 | */
50 | public function delete(UserInterface $user, array $arguments = ['flush'=>true])
51 | {
52 | $this->commonRepository->delete($user, $arguments);
53 | }
54 |
55 | public function findOneById($id)
56 | {
57 | return $this->em->getRepository('AppBundle:User')->find($id);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/AppBundle/Features/Context/MysqlDatabaseContext.php:
--------------------------------------------------------------------------------
1 | em = $em;
26 | }
27 |
28 | /**
29 | * @Then the :entity with id: :id should have been deleted
30 | */
31 | public function theWithIdXShouldHaveBeenDeleted($entity, $id)
32 | {
33 | $className = $this->getClassNameFor($entity);
34 |
35 | Assertions::isNull(
36 | $this->em->getRepository($className)->find($id)
37 | );
38 | }
39 |
40 | /**
41 | * @param $entity
42 | * @return string
43 | */
44 | private function getClassNameFor($entity)
45 | {
46 | switch ($entity) {
47 |
48 | case 'Account':
49 | $entityPath = 'AppBundle:Account';
50 | break;
51 |
52 | case 'File':
53 | $entityPath = 'AppBundle:File';
54 | break;
55 |
56 | default:
57 | throw new \InvalidArgumentException(
58 | sprintf('Unrecognised Entity: %s, did you forget to add a new class name to the switch statement?',
59 | $entity
60 | )
61 | );
62 | }
63 |
64 | return $entityPath;
65 | }
66 | }
--------------------------------------------------------------------------------
/app/config/security.yml:
--------------------------------------------------------------------------------
1 | # To get started with security, check out the documentation:
2 | # http://symfony.com/doc/current/book/security.html
3 | security:
4 |
5 | encoders:
6 | FOS\UserBundle\Model\UserInterface: bcrypt
7 |
8 | role_hierarchy:
9 | ROLE_ADMIN: ROLE_USER
10 | ROLE_SUPER_ADMIN: ROLE_ADMIN
11 |
12 | providers:
13 | fos_userbundle:
14 | id: fos_user.user_provider.username_email
15 |
16 | firewalls:
17 | dev:
18 | pattern: ^/(_(profiler|wdt|error)|css|images|js)/
19 | security: false
20 |
21 | # activate different ways to authenticate
22 |
23 | # http_basic: ~
24 | # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
25 |
26 | # form_login: ~
27 | # http://symfony.com/doc/current/cookbook/security/form_login_setup.html
28 |
29 | api_docs:
30 | pattern: ^/doc
31 | anonymous: true
32 |
33 | api_login:
34 | pattern: ^/login
35 | stateless: true
36 | anonymous: true
37 | form_login:
38 | check_path: /login
39 | require_previous_session: false
40 | username_parameter: username
41 | password_parameter: password
42 | success_handler: lexik_jwt_authentication.handler.authentication_success
43 | failure_handler: lexik_jwt_authentication.handler.authentication_failure
44 |
45 | api:
46 | pattern: ^/
47 | stateless: true
48 | lexik_jwt: ~
49 |
50 |
51 | access_control:
52 | - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
53 | - { path: ^/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY }
54 | - { path: ^/, roles: IS_AUTHENTICATED_FULLY }
--------------------------------------------------------------------------------
/src/AppBundle/Model/AccountInterface.php:
--------------------------------------------------------------------------------
1 | userManager = $userManager;
26 | $this->em = $em;
27 | }
28 |
29 | /**
30 | * @Given there are Users with the following details:
31 | */
32 | public function thereAreUsersWithTheFollowingDetails(TableNode $users)
33 | {
34 | foreach ($users->getColumnsHash() as $key => $val) {
35 |
36 | $user = $this->userManager->createUser();
37 |
38 | $user->setEnabled(true);
39 | $user->setUsername($val['username']);
40 | $user->setEmail($val['email']);
41 | $user->setPlainPassword($val['password']);
42 |
43 | $this->userManager->updateUser($user);
44 |
45 |
46 | $qb = $this->em->createQueryBuilder();
47 |
48 | $query = $qb->update('AppBundle:User', 'u')
49 | ->set('u.id', $qb->expr()->literal($val['uid']))
50 | ->where('u.username = :username')
51 | ->andWhere('u.email = :email')
52 | ->setParameters([
53 | 'username' => $val['username'],
54 | 'email' => $val['email']
55 | ])
56 | ->getQuery();
57 |
58 | $query->execute();
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/AppBundle/Form/Transformer/EntityToIdObjectTransformer.php:
--------------------------------------------------------------------------------
1 | repository = $repository;
27 | }
28 |
29 | /**
30 | * Do nothing.
31 | *
32 | * @param object|null $object
33 | *
34 | * @return string
35 | */
36 | public function transform($object)
37 | {
38 | return '';
39 | }
40 |
41 | /**
42 | * Transforms an array including an identifier to an object.
43 | *
44 | * @param array $idObject
45 | *
46 | * @return object|null
47 | *
48 | * @throws TransformationFailedException if object is not found.
49 | */
50 | public function reverseTransform($idObject)
51 | {
52 | if (!is_array($idObject)) {
53 | return false;
54 | }
55 |
56 | if ( ! array_key_exists('id', $idObject)) {
57 | throw new TransformationFailedException('Unable to find an ID key / value pair on passed in $idObject');
58 | }
59 |
60 | $object = $this->repository->findOneById($idObject['id']);
61 |
62 | if (null === $object) {
63 | throw new TransformationFailedException(sprintf(
64 | 'A "%s" with ID "%s" does not exist!',
65 | get_class($object),
66 | $idObject['id']
67 | ));
68 | }
69 |
70 | return $object;
71 | }
72 | }
--------------------------------------------------------------------------------
/src/AppBundle/Form/Type/AccountType.php:
--------------------------------------------------------------------------------
1 | userRepository = $userRepository;
27 | }
28 |
29 | /**
30 | * @param FormBuilderInterface $builder
31 | * @param array $options
32 | */
33 | public function buildForm(FormBuilderInterface $builder, array $options)
34 | {
35 | $builder
36 | ->add('name', TextType::class)
37 | ;
38 |
39 | $userTransformer = new EntityToIdObjectTransformer($this->userRepository);
40 | $userCollectionTransformer = new ManyEntityToIdObjectTransformer($userTransformer);
41 |
42 | $builder
43 | ->add(
44 | $builder->create('users', TextType::class)->addModelTransformer($userCollectionTransformer)
45 | )
46 | ;
47 |
48 | // ->add('files', EntityType::class, [
49 | // 'class' => 'AppBundle\Entity\File',
50 | // "property" => "id",
51 | // "multiple" => true,
52 | // ])
53 | ;
54 | }
55 |
56 | public function configureOptions(OptionsResolver $resolver)
57 | {
58 | $resolver->setDefaults([
59 | 'data_class' => 'AppBundle\DTO\AccountDTO',
60 | ]);
61 | }
62 |
63 | public function getName()
64 | {
65 | return 'account';
66 | }
67 | }
--------------------------------------------------------------------------------
/spec/AppBundle/Security/Authorisation/Voter/UserVoterSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType('AppBundle\Security\Authorisation\Voter\UserVoter');
17 | $this->shouldImplement('Symfony\Component\Security\Core\Authorization\Voter\VoterInterface');
18 | }
19 |
20 | function it_supports_expected_attributes()
21 | {
22 | $this->supportsAttribute('view')->shouldReturn(true);
23 | }
24 |
25 | function it_doesnt_support_unexpected_attributes()
26 | {
27 | $this->supportsAttribute('1')->shouldReturn(false);
28 | }
29 |
30 | function it_supports_expected_class(User $user)
31 | {
32 | $this->supportsClass($user)->shouldReturn(true);
33 | }
34 |
35 | function it_doesnt_support_unexpected_class()
36 | {
37 | $this->supportsClass(new \StdClass())->shouldReturn(false);
38 | }
39 |
40 | function it_abstains_if_doesnt_support_attribute(TokenInterface $token, User $user)
41 | {
42 | $this->vote($token, $user, ['unsupported'])->shouldReturn(VoterInterface::ACCESS_ABSTAIN);
43 | }
44 |
45 | function it_abstains_if_doesnt_support_this_class(TokenInterface $token)
46 | {
47 | $this->vote($token, new \StdClass(), [UserVoter::VIEW])->shouldReturn(VoterInterface::ACCESS_ABSTAIN);
48 | }
49 |
50 | function it_denies_if_not_matching(TokenInterface $token, User $user1, User $user2)
51 | {
52 | $token->getUser()->willReturn($user2);
53 |
54 | $this->vote($token, $user1, [UserVoter::VIEW])->shouldReturn(VoterInterface::ACCESS_DENIED);
55 | }
56 |
57 | function it_grants_if_matching(TokenInterface $token, User $user)
58 | {
59 | $token->getUser()->willReturn($user);
60 |
61 | $this->vote($token, $user, [UserVoter::VIEW])->shouldReturn(VoterInterface::ACCESS_GRANTED);
62 | }
63 | }
--------------------------------------------------------------------------------
/app/AppKernel.php:
--------------------------------------------------------------------------------
1 | getEnvironment(), ['dev', 'test', 'acceptance'], true)) {
32 | $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
33 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
34 | $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
35 | $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
36 | }
37 |
38 | return $bundles;
39 | }
40 |
41 | public function getRootDir()
42 | {
43 | return __DIR__;
44 | }
45 |
46 | public function getCacheDir()
47 | {
48 | return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
49 | }
50 |
51 | public function getLogDir()
52 | {
53 | return dirname(__DIR__).'/var/logs';
54 | }
55 |
56 | public function registerContainerConfiguration(LoaderInterface $loader)
57 | {
58 | $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/AppBundle/Handler/UserHandler.php:
--------------------------------------------------------------------------------
1 | formHandler = $formHandler;
27 | $this->repository = $userRepositoryInterface;
28 | }
29 |
30 | public function get($id)
31 | {
32 | return $this->repository->findOneById($id);
33 | }
34 |
35 | public function all($limit = 10, $offset = 0)
36 | {
37 | throw new \DomainException('UserHandler::all is currently not implemented.');
38 | }
39 |
40 | public function post(array $parameters, array $options = [])
41 | {
42 | throw new \DomainException('UserHandler::post is currently not implemented.');
43 | }
44 |
45 | public function put($resource, array $parameters, array $options = [])
46 | {
47 | throw new \DomainException('UserHandler::put is currently not implemented.');
48 | }
49 |
50 | /**
51 | * @param UserInterface $user
52 | * @param array $parameters
53 | * @param array $options
54 | * @return UserInterface
55 | */
56 | public function patch($user, array $parameters, array $options = [])
57 | {
58 | if ( ! $user instanceof UserInterface) {
59 | throw new \InvalidArgumentException('Not a valid User');
60 | }
61 |
62 | $user = $this->formHandler->handle(
63 | $user,
64 | $parameters,
65 | Request::METHOD_PATCH,
66 | $options
67 | );
68 |
69 | $this->repository->save($user);
70 |
71 | return $user;
72 | }
73 |
74 | public function delete($resource)
75 | {
76 | throw new \DomainException('UserHandler::delete is currently not implemented.');
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/src/AppBundle/Security/Authorization/Voter/FileVoter.php:
--------------------------------------------------------------------------------
1 | supportsClass(get_class($requestedFile))) {
50 | return VoterInterface::ACCESS_ABSTAIN;
51 | }
52 |
53 | // set the attribute to check against
54 | $attribute = $attributes[0];
55 |
56 | // check if the given attribute is covered by this voter
57 | if (!$this->supportsAttribute($attribute)) {
58 | return VoterInterface::ACCESS_ABSTAIN;
59 | }
60 |
61 | // get current logged in user
62 | $loggedInUser = $token->getUser(); /** @var $loggedInUser UserInterface */
63 |
64 | try {
65 | if ($requestedFile->belongsToUser($loggedInUser)) {
66 |
67 | return VoterInterface::ACCESS_GRANTED;
68 | }
69 | } catch (\Exception $e) {
70 |
71 | return VoterInterface::ACCESS_DENIED;
72 | }
73 |
74 | return VoterInterface::ACCESS_DENIED;
75 | }
76 |
77 | }
--------------------------------------------------------------------------------
/src/AppBundle/Security/Authorization/Voter/AccountVoter.php:
--------------------------------------------------------------------------------
1 | supportsClass(get_class($requestedAccount))) {
53 | return VoterInterface::ACCESS_ABSTAIN;
54 | }
55 |
56 | // set the attribute to check against
57 | $attribute = $attributes[0];
58 |
59 | // check if the given attribute is covered by this voter
60 | if (!$this->supportsAttribute($attribute)) {
61 | return VoterInterface::ACCESS_ABSTAIN;
62 | }
63 |
64 | // get current logged in user
65 | $loggedInUser = $token->getUser(); /** @var $loggedInUser UserInterface */
66 |
67 | // make sure that this User has access to this account
68 | if ($loggedInUser->hasAccount($requestedAccount)) {
69 | return VoterInterface::ACCESS_GRANTED;
70 | }
71 |
72 | return VoterInterface::ACCESS_DENIED;
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/spec/AppBundle/Handler/UserHandlerSpec.php:
--------------------------------------------------------------------------------
1 | formHandler = $formHandler;
23 | $this->repository = $repository;
24 |
25 | $this->beConstructedWith($repository, $formHandler);
26 | }
27 |
28 | function it_is_initializable()
29 | {
30 | $this->shouldHaveType('AppBundle\Handler\UserHandler');
31 | $this->shouldImplement('AppBundle\Handler\HandlerInterface');
32 | }
33 |
34 | function it_can_GET()
35 | {
36 | $id = 777;
37 | $this->get($id);
38 | $this->repository->find($id)->shouldHaveBeenCalled();
39 | }
40 |
41 | function it_cannot_get_ALL()
42 | {
43 | $this->shouldThrow('\DomainException')->during('all', [1,2]);
44 | }
45 |
46 | function it_cannot_POST()
47 | {
48 | $this->shouldThrow('\DomainException')->during('post', [['param1']]);
49 | }
50 |
51 | function it_cannot_PUT()
52 | {
53 | $this->shouldThrow('\DomainException')->during('put', [['param1'], []]);
54 | }
55 |
56 | function it_should_allow_PATCH(User $user)
57 | {
58 | $params = ['1','2','3'];
59 |
60 | $this->formHandler->handle($user, $params, Request::METHOD_PATCH, [])->willReturn($user);
61 | $this->repository->save(Argument::type('AppBundle\Entity\User'))->shouldBeCalled();
62 |
63 | $this->patch($user, $params)->shouldReturn($user);
64 | }
65 |
66 | function it_should_throw_if_PATCH_not_given_a_valid_user()
67 | {
68 | $this->shouldThrow('\InvalidArgumentException')->during('patch', ['not_user', []]);
69 | $this->shouldThrow('\InvalidArgumentException')->during('patch', [new \StdClass(), [123]]);
70 | }
71 |
72 | function it_cannot_DELETE()
73 | {
74 | $this->shouldThrow('\DomainException')->during('delete', ['something']);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/AppBundle/Model/UploadedFile.php:
--------------------------------------------------------------------------------
1 | filePath = $path;
30 | $this->originalFileName = $originalName;
31 | $this->mimeType = $mimeType;
32 | $this->fileSize = $size;
33 | $this->fileExtension = $extension;
34 | }
35 |
36 | /**
37 | * @param SymfonyUploadedFile $uploadedFile
38 | * @return SymfonyUploadedFile
39 | */
40 | public static function createFromSymfonyUploadedFile(SymfonyUploadedFile $uploadedFile)
41 | {
42 | return new UploadedFile(
43 | //$uploadedFile->getPath(),
44 | $uploadedFile->getPathname(),
45 | $uploadedFile->getClientOriginalName(),
46 | $uploadedFile->getClientMimeType(),
47 | $uploadedFile->getSize(),
48 | $uploadedFile->getClientOriginalExtension()
49 | );
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function getFilePath()
56 | {
57 | return $this->filePath;
58 | }
59 |
60 | /**
61 | * @return string
62 | */
63 | public function getOriginalFileName()
64 | {
65 | return $this->originalFileName;
66 | }
67 |
68 | /**
69 | * @return string
70 | */
71 | public function getFileExtension()
72 | {
73 | return $this->fileExtension;
74 | }
75 |
76 | /**
77 | * @return string
78 | */
79 | public function getMimeType()
80 | {
81 | return $this->mimeType;
82 | }
83 |
84 | /**
85 | * @return int
86 | */
87 | public function getFileSize()
88 | {
89 | return $this->fileSize;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/spec/AppBundle/Repository/Restricted/UserRepositorySpec.php:
--------------------------------------------------------------------------------
1 | userRepository = $userRepository;
22 | $this->authorizationChecker = $authorizationChecker;
23 |
24 | $this->beConstructedWith($userRepository, $authorizationChecker);
25 | }
26 |
27 | function it_is_initializable()
28 | {
29 | $this->shouldHaveType('AppBundle\Repository\Restricted\UserRepository');
30 | $this->shouldImplement('AppBundle\Repository\UserRepositoryInterface');
31 | }
32 |
33 | function it_can_find_logged_in_user_by_id(UserInterface $user)
34 | {
35 | $id = 1;
36 |
37 | $this->userRepository->find($id)->willReturn($user);
38 | $this->authorizationChecker->isGranted('view', $user)->willReturn(true);
39 |
40 | $this->find($id)->shouldReturn($user);
41 | }
42 |
43 | function it_cannot_find_any_other_user(UserInterface $user)
44 | {
45 | $id = 6;
46 |
47 | $this->userRepository->find($id)->willReturn($user);
48 | $this->authorizationChecker->isGranted('view', $user)->willReturn(false);
49 |
50 | $this->shouldThrow('Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException')
51 | ->during('find', [$id]);
52 | }
53 |
54 | function it_can_save_logged_in_user(UserInterface $user)
55 | {
56 | $this->authorizationChecker->isGranted('view', $user)->willReturn(true);
57 |
58 | $this->save($user);
59 | $this->userRepository->save($user,['flush'=>true])->shouldHaveBeenCalled();
60 | }
61 |
62 | function it_cannot_save_any_other_user(UserInterface $user)
63 | {
64 | $this->authorizationChecker->isGranted('view', $user)->willReturn(false);
65 |
66 | $this->shouldThrow('Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException')
67 | ->during('save', [$user,['flush'=>true]]);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/AppBundle/DTO/AccountDTO.php:
--------------------------------------------------------------------------------
1 | users = new ArrayCollection();
30 | // $this->files = new ArrayCollection();
31 | }
32 |
33 | /**
34 | * @return mixed
35 | */
36 | public function getDataClass()
37 | {
38 | return self::class;
39 | }
40 |
41 | /**
42 | * @return string
43 | */
44 | public function jsonSerialize()
45 | {
46 | return [
47 | 'name' => $this->name,
48 | 'users' => $this->users,
49 | // 'files' => $this->files,
50 | ];
51 | }
52 |
53 | /**
54 | * @return ArrayCollection
55 | */
56 | public function getUsers()
57 | {
58 | return $this->users;
59 | }
60 |
61 | /**
62 | * @param Array $users
63 | * @return $this
64 | */
65 | public function setUsers($users)
66 | {
67 | $this->users = $users;
68 |
69 | return $this;
70 | }
71 |
72 | /**
73 | * @return mixed
74 | */
75 | public function getName()
76 | {
77 | return $this->name;
78 | }
79 |
80 | /**
81 | * @param mixed $name
82 | * @return $this
83 | */
84 | public function setName($name)
85 | {
86 | $this->name = $name;
87 |
88 | return $this;
89 | }
90 |
91 | // /**
92 | // * @return array
93 | // */
94 | // public function getFiles()
95 | // {
96 | // return $this->files;
97 | // }
98 | //
99 | // /**
100 | // * @param array $files
101 | // * @return $this
102 | // */
103 | // public function setFiles($files)
104 | // {
105 | // $this->files = $files;
106 | //
107 | // return $this;
108 | // }
109 | }
110 |
--------------------------------------------------------------------------------
/src/AppBundle/Repository/Doctrine/DoctrineAccountRepository.php:
--------------------------------------------------------------------------------
1 | commonRepository = $commonRepository;
34 | $this->accountEntityRepository = $accountEntityRepository;
35 | }
36 |
37 | /**
38 | * @param AccountInterface $account
39 | */
40 | public function refresh(AccountInterface $account)
41 | {
42 | $this->commonRepository->refresh($account);
43 | }
44 |
45 | /**
46 | * @param AccountInterface $account
47 | * @param array $arguments
48 | */
49 | public function save(AccountInterface $account, array $arguments = ['flush'=>true])
50 | {
51 | $this->commonRepository->save($account, $arguments);
52 | }
53 |
54 | /**
55 | * @param AccountInterface $account
56 | * @param array $arguments
57 | */
58 | public function delete(AccountInterface $account, array $arguments = ['flush'=>true])
59 | {
60 | $this->commonRepository->delete($account, $arguments);
61 | }
62 |
63 | /**
64 | * @param $id
65 | * @return mixed
66 | */
67 | public function findOneById($id)
68 | {
69 | return $this->accountEntityRepository->find($id);
70 | }
71 |
72 | /**
73 | * @param UserInterface $user
74 | * @return array
75 | */
76 | public function findAllForUser(UserInterface $user)
77 | {
78 | return $this->accountEntityRepository->findAllForUser($user);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/AppBundle/Repository/Doctrine/DoctrineFileRepository.php:
--------------------------------------------------------------------------------
1 | commonRepository = $commonRepository;
36 | $this->fileEntityRepository = $fileEntityRepository;
37 | }
38 |
39 | /**
40 | * @param FileInterface $file
41 | */
42 | public function refresh(FileInterface $file)
43 | {
44 | $this->commonRepository->refresh($file);
45 | }
46 |
47 | /**
48 | * @param FileInterface $file
49 | * @param array $arguments
50 | */
51 | public function save(FileInterface $file, array $arguments = ['flush'=>true])
52 | {
53 | $this->commonRepository->save($file, $arguments);
54 | }
55 |
56 | /**
57 | * @param FileInterface $file
58 | * @param array $arguments
59 | */
60 | public function delete(FileInterface $file, array $arguments = ['flush'=>true])
61 | {
62 | $this->commonRepository->delete($file, $arguments);
63 | }
64 |
65 | /**
66 | * @param $id
67 | * @return mixed
68 | */
69 | public function findOneById($id)
70 | {
71 | return $this->fileEntityRepository->find($id);
72 | }
73 |
74 | /**
75 | * @param AccountInterface $account
76 | * @return array
77 | */
78 | public function findAllForAccount(AccountInterface $account)
79 | {
80 | return $this->fileEntityRepository->findAllForAccount($account);
81 | }
82 |
83 | /**
84 | * @param UserInterface $user
85 | * @return mixed
86 | */
87 | public function findAllForUser(UserInterface $user)
88 | {
89 | return $this->fileEntityRepository->findAllForUser($user);
90 | }
91 | }
--------------------------------------------------------------------------------
/src/AppBundle/Repository/Restricted/RestrictedAccountRepository.php:
--------------------------------------------------------------------------------
1 | repository = $repository;
33 | $this->authorizationChecker = $authorizationChecker;
34 | }
35 |
36 | /**
37 | * @param AccountInterface $account
38 | * @return mixed
39 | */
40 | public function refresh(AccountInterface $account)
41 | {
42 | $this->authorizationChecker->isGranted('view', $account);
43 |
44 | $this->repository->refresh($account);
45 | }
46 |
47 | /**
48 | * @param AccountInterface $account
49 | * @param array $arguments
50 | */
51 | public function save(AccountInterface $account, array $arguments = [])
52 | {
53 | $this->authorizationChecker->isGranted('view', $account);
54 |
55 | $this->repository->save($account);
56 | }
57 |
58 | /**
59 | * @param AccountInterface $account
60 | * @param array $arguments
61 | */
62 | public function delete(AccountInterface $account, array $arguments = [])
63 | {
64 | $this->authorizationChecker->isGranted('view', $account);
65 |
66 | $this->repository->delete($account);
67 | }
68 |
69 | /**
70 | * @param $id
71 | * @return mixed|null
72 | */
73 | public function findOneById($id)
74 | {
75 | $account = $this->repository->findOneById($id);
76 |
77 | $this->denyAccessUnlessGranted('view', $account);
78 |
79 | return $account;
80 | }
81 |
82 | /**
83 | * @param UserInterface $user
84 | * @return mixed
85 | */
86 | public function findAllForUser(UserInterface $user)
87 | {
88 | $accounts = $this->repository->findAllForUser($user);
89 |
90 | foreach ($accounts as $account) {
91 | $this->denyAccessUnlessGranted('view', $account);
92 | }
93 |
94 | return $accounts;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/AppBundle/Repository/Restricted/RestrictedFileRepository.php:
--------------------------------------------------------------------------------
1 | repository = $repository;
29 | $this->authorizationChecker = $authorizationChecker;
30 | }
31 |
32 | /**
33 | * @param FileInterface $file
34 | * @return mixed
35 | */
36 | public function refresh(FileInterface $file)
37 | {
38 | $this->denyAccessUnlessGranted('view', $file);
39 |
40 | $this->repository->refresh($file);
41 | }
42 |
43 | /**
44 | * @param FileInterface $file
45 | * @param array $arguments
46 | */
47 | public function save(FileInterface $file, array $arguments = ['flush'=>true])
48 | {
49 | $this->denyAccessUnlessGranted('view', $file);
50 |
51 | $this->repository->save($file, $arguments);
52 | }
53 |
54 | /**
55 | * @param FileInterface $file
56 | * @param array $arguments
57 | */
58 | public function delete(FileInterface $file, array $arguments = ['flush'=>true])
59 | {
60 | $this->denyAccessUnlessGranted('view', $file);
61 |
62 | $this->repository->delete($file, $arguments);
63 | }
64 |
65 | /**
66 | * @param $id
67 | * @return mixed|null
68 | */
69 | public function findOneById($id)
70 | {
71 | $file = $this->repository->findOneById($id);
72 |
73 | $this->denyAccessUnlessGranted('view', $file);
74 |
75 | return $file;
76 | }
77 |
78 | /**
79 | * @param AccountInterface $account
80 | * @return mixed
81 | */
82 | public function findAllForAccount(AccountInterface $account)
83 | {
84 | $files = $this->repository->findAllForAccount($account);
85 |
86 | foreach ($files as $file) {
87 | $this->denyAccessUnlessGranted('view', $file);
88 | }
89 |
90 | return $files;
91 | }
92 |
93 | /**
94 | * @param UserInterface $user
95 | * @return mixed
96 | */
97 | public function findAllForUser(UserInterface $user)
98 | {
99 | $files = $this->repository->findAllForUser($user);
100 |
101 | foreach ($files as $file) {
102 | $this->denyAccessUnlessGranted('view', $file);
103 | }
104 |
105 | return $files;
106 | }
107 | }
--------------------------------------------------------------------------------
/spec/AppBundle/Event/Listener/ContentTypeListenerSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType('AppBundle\Event\Listener\ContentTypeListener');
16 | }
17 |
18 | function it_throws_if_not_matching_other_rules(GetResponseEvent $event)
19 | {
20 | $request = new Request();
21 | $request->headers->set('Content-type', 'blah');
22 | $request->setMethod(Request::METHOD_PUT);
23 |
24 | $event->getRequest()->willReturn($request);
25 |
26 | $this->shouldThrow('AppBundle\Exception\HttpContentTypeException')
27 | ->during('onKernelRequest', [$event]);
28 | }
29 |
30 | function it_throws_if_no_content_type_and_is_not_GET(GetResponseEvent $event)
31 | {
32 | $request = new Request();
33 | $request->setMethod(Request::METHOD_POST);
34 |
35 | $event->getRequest()->willReturn($request);
36 |
37 | $this->shouldThrow('AppBundle\Exception\HttpContentTypeException')
38 | ->during('onKernelRequest', [$event]);
39 | }
40 |
41 | function it_expects_a_valid_content_type_header(GetResponseEvent $event)
42 | {
43 | $request = new Request();
44 | $request->headers->set('Content-type', ContentTypeListener::MIME_TYPE_APPLICATION_JSON);
45 |
46 | $event->getRequest()->willReturn($request);
47 |
48 | $this->onKernelRequest($event)->shouldReturn(true);
49 | }
50 |
51 | function it_should_ignore_method_type_GET(GetResponseEvent $event)
52 | {
53 | $request = new Request();
54 | $request->headers->set('Content-type', 'fake');
55 | $request->setMethod(Request::METHOD_GET);
56 |
57 | $event->getRequest()->willReturn($request);
58 |
59 | $this->onKernelRequest($event)->shouldReturn(true);
60 | }
61 |
62 | function it_should_deny_multipart_form_data_for_none_file_route(GetResponseEvent $event)
63 | {
64 | // $request = new Request();
65 | // $request->request->set('_route', 'some_none_file_route');
66 | // $request->headers->set('Content-type', ContentTypeListener::MIME_TYPE_MULTIPART_FORM_DATA);
67 | // $request->setMethod(Request::METHOD_POST);
68 | //
69 | // $event->getRequest()->willReturn($request);
70 | //
71 | // $this->shouldThrow('AppBundle\Exception\HttpContentTypeException')
72 | // ->during('onKernelRequest', [$event]);
73 | }
74 |
75 | function it_should_allow_multipart_form_data_for_file_routes(GetResponseEvent $event)
76 | {
77 | // $request = new Request();
78 | // $request->request->set('_route', 'post_accounts_files');
79 | // $request->headers->set('Content-type', ContentTypeListener::MIME_TYPE_MULTIPART_FORM_DATA);
80 | // $request->setMethod(Request::METHOD_POST);
81 | //
82 | // $event->getRequest()->willReturn($request);
83 | //
84 | // $this->onKernelRequest($event)->shouldReturn(true);
85 | }
86 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "a6_chris/symfony.rest.example.dev",
3 | "license": "proprietary",
4 | "type": "project",
5 | "autoload": {
6 | "psr-4": {
7 | "": "src/"
8 | },
9 | "classmap": [
10 | "app/AppKernel.php",
11 | "app/AppCache.php"
12 | ]
13 | },
14 | "autoload-dev": {
15 | "psr-4": {
16 | "Tests\\": "tests/"
17 | }
18 | },
19 | "require": {
20 | "php": ">=5.5.9",
21 | "symfony/symfony": "3.0.*",
22 | "doctrine/orm": "^2.5",
23 | "doctrine/doctrine-bundle": "^1.6",
24 | "doctrine/doctrine-cache-bundle": "^1.2",
25 | "symfony/swiftmailer-bundle": "^2.3",
26 | "symfony/monolog-bundle": "^2.8",
27 | "sensio/distribution-bundle": "^5.0",
28 | "sensio/framework-extra-bundle": "^3.0.2",
29 | "incenteev/composer-parameter-handler": "^2.0",
30 |
31 | "nelmio/cors-bundle": "^1.4",
32 | "nelmio/api-doc-bundle": "^2.11",
33 | "friendsofsymfony/rest-bundle": "^1.7",
34 | "csa/guzzle-bundle": "^1.3",
35 | "jms/serializer-bundle": "^1.1",
36 | "oneup/flysystem-bundle": "^1.2",
37 | "friendsofsymfony/user-bundle": "dev-master",
38 | "lexik/jwt-authentication-bundle": "dev-master"
39 |
40 | },
41 | "require-dev": {
42 | "sensio/generator-bundle": "^3.0",
43 | "symfony/phpunit-bridge": "^2.7",
44 |
45 | "phpspec/phpspec": "^2.4",
46 | "behat/behat": "dev-master",
47 |
48 | "behat/symfony2-extension": "^2.1",
49 | "phpunit/phpunit": "^5.1"
50 | },
51 | "minimum-stability": "stable",
52 | "scripts": {
53 | "post-install-cmd": [
54 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
55 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
56 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
57 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
58 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
59 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
60 | ],
61 | "post-update-cmd": [
62 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
63 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
64 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
65 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
66 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
67 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
68 | ]
69 | },
70 | "extra": {
71 | "symfony-app-dir": "app",
72 | "symfony-bin-dir": "bin",
73 | "symfony-var-dir": "var",
74 | "symfony-web-dir": "web",
75 | "symfony-tests-dir": "tests",
76 | "symfony-assets-install": "relative",
77 | "incenteev-parameters": {
78 | "file": "app/config/parameters.yml"
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Symfony 3 RESTful API Example
2 | =============================
3 |
4 | This is the code for the [Symfony 3 REST Tutorial][1] at CodeReviewVideos.com.
5 |
6 | ## About The Course
7 |
8 | In this series we are going to take most of what has been taught on CodeReviewVideos so far, and use it to implement the foundation of a Symfony-based RESTful API.
9 |
10 | This is not a good starting point if you have never used Symfony before. If you are a beginner, please try the [Symfony Tutorial for Beginners][2] before attempting to create a RESTful API.
11 |
12 | Whilst very new, we are going to proceed with Symfony 3 for this project. However, if you are still using Symfony 2, you should have no trouble following along. If anything, your life may be that little bit easier ;)
13 |
14 | By the end of this series you will have created a RESTful API with FOSRESTBundle, guided by tests using Behat 3 and PHPSpec 2. The API itself will exposes Users - via FOSUserBundle - along with Accounts, and File Uploading using Flysystem.
15 |
16 | You will have gained an understanding into how to add further 'modules' to your API, allowing you to create custom end points to meet the needs of your specific application.
17 |
18 | As with many things Symfony, there are multiple ways to achieve the goals in this series. We could switch out FOSRESTBundle for DunglasAPIBundle. We could switch Flysystem for Guafrette. You are entirely free to do so.
19 |
20 | This API is going to be based on FOSRESTBundle. The main reasoning for this is that I have already covered this bundle in another dedicated tutorial series, so if you get stuck, there are plenty of lessons explaining the general setup.
21 |
22 | You may be wondering about the difference between a User and an Account. The idea here is that every User would have their own User Profile, but may belong to one of more Accounts. This is pretty helpful in the real world, and opens up options for your app as it grows. Feel free to rip this part out if you don't need it.
23 |
24 | Log in is going to be a little different here. When a User POST's in a valid username and password combo, they will receive a JSON Web Token / JWT (pronounced Jot), which they will then need to use as part of any future request. Don't worry, we will cover this in full, and you will see it's really not that difficult at all. The reasoning for doing this is that our front end will thank us for it.
25 |
26 | To ensure stability, we will be using VirtualBox with an Ansible build script. This will - hopefully - mean getting from development to production is largely taken care of.
27 |
28 | Lastly, we will cover how to interact with this API using ReactJS. You could switch this out for Angular, Ember, a mobile Application (e.g. Ionic, or a native app), or any other front end framework. That choice is entirely up to you. I am by far and away not the world's best JavaScript guy, so take this section as an example, rather than a defacto standard.
29 |
30 | Hopefully you will find this exercise useful and practical. Please feel free to leave comments, ask questions, or get in touch if you would like to know more about a specific topic.
31 |
32 | [Click here to watch all the videos in this series][1]
33 |
34 |
35 | [1]: https://www.codereviewvideos.com/course/symfony-3-rest-tutorial
36 | [2]: https://www.codereviewvideos.com/course/beginner-friendly-hands-on-symfony-3-tutorial
--------------------------------------------------------------------------------
/web/.htaccess:
--------------------------------------------------------------------------------
1 | # Use the front controller as index file. It serves as a fallback solution when
2 | # every other rewrite/redirect fails (e.g. in an aliased environment without
3 | # mod_rewrite). Additionally, this reduces the matching process for the
4 | # start page (path "/") because otherwise Apache will apply the rewriting rules
5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
6 | DirectoryIndex app.php
7 |
8 | # By default, Apache does not evaluate symbolic links if you did not enable this
9 | # feature in your server configuration. Uncomment the following line if you
10 | # install assets as symlinks or if you experience problems related to symlinks
11 | # when compiling LESS/Sass/CoffeScript assets.
12 | # Options FollowSymlinks
13 |
14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve
15 | # to the front controller "/app.php" but be rewritten to "/app.php/app".
16 |
17 | Options -MultiViews
18 |
19 |
20 |
21 | RewriteEngine On
22 |
23 | # Determine the RewriteBase automatically and set it as environment variable.
24 | # If you are using Apache aliases to do mass virtual hosting or installed the
25 | # project in a subdirectory, the base path will be prepended to allow proper
26 | # resolution of the app.php file and to redirect to the correct URI. It will
27 | # work in environments without path prefix as well, providing a safe, one-size
28 | # fits all solution. But as you do not need it in this case, you can comment
29 | # the following 2 lines to eliminate the overhead.
30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
31 | RewriteRule ^(.*) - [E=BASE:%1]
32 |
33 | # Sets the HTTP_AUTHORIZATION header removed by Apache
34 | RewriteCond %{HTTP:Authorization} .
35 | RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
36 |
37 | # Redirect to URI without front controller to prevent duplicate content
38 | # (with and without `/app.php`). Only do this redirect on the initial
39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an
40 | # endless redirect loop (request -> rewrite to front controller ->
41 | # redirect -> request -> ...).
42 | # So in case you get a "too many redirects" error or you always get redirected
43 | # to the start page because your Apache does not expose the REDIRECT_STATUS
44 | # environment variable, you have 2 choices:
45 | # - disable this feature by commenting the following 2 lines or
46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
47 | # following RewriteCond (best solution)
48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$
49 | RewriteRule ^app\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
50 |
51 | # If the requested filename exists, simply serve it.
52 | # We only want to let Apache serve files and not directories.
53 | RewriteCond %{REQUEST_FILENAME} -f
54 | RewriteRule ^ - [L]
55 |
56 | # Rewrite all other queries to the front controller.
57 | RewriteRule ^ %{ENV:BASE}/app.php [L]
58 |
59 |
60 |
61 |
62 | # When mod_rewrite is not available, we instruct a temporary redirect of
63 | # the start page to the front controller explicitly so that the website
64 | # and the generated links can still be used.
65 | RedirectMatch 302 ^/$ /app.php/
66 | # RedirectTemp cannot be used instead
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/AppBundle/Features/Context/FileSetupContext.php:
--------------------------------------------------------------------------------
1 | userManager = $userManager;
55 | $this->fileFactory = $fileFactory;
56 | $this->em = $em;
57 | $this->filesystem = $filesystem;
58 | $this->dummyDataPath = $dummyDataPath;
59 | }
60 |
61 | /**
62 | * @AfterScenario
63 | */
64 | public static function removeUploadedFiles()
65 | {
66 | foreach (glob(__DIR__ . '/../../../../uploads/*') as $file) {
67 | echo "Removing uploaded file: \n";
68 | echo basename($file) . "\n";
69 | unlink($file);
70 | }
71 | }
72 |
73 | /**
74 | * @Given there are files with the following details:
75 | */
76 | public function thereAreFilesWithTheFollowingDetails(TableNode $files)
77 | {
78 | foreach ($files->getColumnsHash() as $key => $val) {
79 |
80 | $file = $this->fileFactory->create(
81 | $val['originalFileName'],
82 | $val['internalFileName'],
83 | $val['guessedExtension'],
84 | $val['size']
85 | );
86 |
87 | $this->em->persist($file);
88 | $this->em->flush();
89 |
90 | $qb = $this->em->createQueryBuilder();
91 |
92 | $query = $qb->update('AppBundle:File', 'f')
93 | ->set('f.id', $qb->expr()->literal($val['uid']))
94 | ->where('f.internalFileName = :internalFileName')
95 | ->setParameters([
96 | 'internalFileName' => $val['internalFileName'],
97 | ])
98 | ->getQuery()
99 | ;
100 |
101 | $query->execute();
102 |
103 | if ( ! empty($val['dummyFile'])) {
104 | $this->filesystem->put(
105 | $val['internalFileName'],
106 | file_get_contents($this->dummyDataPath . $val['dummyFile'])
107 | );
108 | }
109 | }
110 |
111 | $this->em->flush();
112 | }
113 | }
--------------------------------------------------------------------------------
/src/AppBundle/Controller/UsersController.php:
--------------------------------------------------------------------------------
1 | getUserHandler()->get($id);
46 |
47 | $view = $this->view($user);
48 |
49 | return $view;
50 | }
51 |
52 | /**
53 | * Gets a collection of Users.
54 | *
55 | * @ApiDoc(
56 | * output = "AppBundle\Entity\User",
57 | * statusCodes = {
58 | * 405 = "Method not allowed"
59 | * }
60 | * )
61 | *
62 | * @throws MethodNotAllowedHttpException
63 | *
64 | * @return View
65 | */
66 | public function cgetAction()
67 | {
68 | throw new MethodNotAllowedHttpException([], "Method not allowed");
69 | }
70 |
71 | /**
72 | * Update existing User from the submitted data
73 | *
74 | * @ApiDoc(
75 | * resource = true,
76 | * input = "AppBundle\Form\UserType",
77 | * statusCodes = {
78 | * 204 = "Returned when successful",
79 | * 400 = "Returned when errors",
80 | * 401 = "Returned when provided password is incorrect",
81 | * 404 = "Returned when not found"
82 | * }
83 | * )
84 | *
85 | * @param Request $request the request object
86 | * @param int $id the user id
87 | *
88 | * @return FormTypeInterface|RouteRedirectView
89 | *
90 | * @throws NotFoundHttpException when does not exist
91 | */
92 | public function patchAction(Request $request, $id)
93 | {
94 | $requestedUser = $this->get('crv.repository.restricted_user_repository')->findOneById($id);
95 |
96 | try {
97 |
98 | $statusCode = Response::HTTP_NO_CONTENT;
99 |
100 | /** @var $user \AppBundle\Entity\User */
101 | $user = $this->getUserHandler()->patch(
102 | $requestedUser,
103 | $request->request->all()
104 | );
105 |
106 | $routeOptions = array(
107 | 'id' => $user->getId(),
108 | '_format' => $request->get('_format')
109 | );
110 |
111 | return $this->routeRedirectView('get_users', $routeOptions, $statusCode);
112 |
113 | } catch (InvalidFormException $e) {
114 |
115 | return $e->getForm();
116 | }
117 | }
118 |
119 | /**
120 | * @return UserHandler
121 | */
122 | private function getUserHandler()
123 | {
124 | return $this->container->get('crv.handler.restricted_user_handler');
125 | }
126 | }
--------------------------------------------------------------------------------
/src/AppBundle/Features/Context/AccountSetupContext.php:
--------------------------------------------------------------------------------
1 | userManager = $userManager;
41 | $this->accountFactory = $accountFactory;
42 | $this->em = $em;
43 | }
44 |
45 | /**
46 | * @Given there are Accounts with the following details:
47 | */
48 | public function thereAreAccountsWithTheFollowingDetails(TableNode $accounts)
49 | {
50 | foreach ($accounts->getColumnsHash() as $key => $val) {
51 |
52 | $account = $this->accountFactory->create($val['name']);
53 |
54 | $this->em->persist($account);
55 | $this->em->flush();
56 |
57 |
58 | $this->fixIdForAccountNamed($val['uid'], $val['name']);
59 |
60 |
61 | $account = $this->em->getRepository('AppBundle:Account')->find($val['uid']);
62 |
63 | $this->addUsersToAccount($val['users'], $account);
64 |
65 | if (isset($val['files'])) {
66 | $this->addFilesToAccount($val['files'], $account);
67 | }
68 | }
69 |
70 | $this->em->flush();
71 | }
72 |
73 | private function fixIdForAccountNamed($id, $accountName)
74 | {
75 | $qb = $this->em->createQueryBuilder();
76 |
77 | $query = $qb->update('AppBundle:Account', 'a')
78 | ->set('a.id', $qb->expr()->literal($id))
79 | ->where('a.name = :accountName')
80 | ->setParameters([
81 | 'accountName' => $accountName,
82 | ])
83 | ->getQuery()
84 | ;
85 |
86 | $query->execute();
87 | }
88 |
89 | private function addUsersToAccount($userIds, Account $account)
90 | {
91 | $userIds = explode(',', $userIds);
92 |
93 | if (empty($userIds)) {
94 | return false;
95 | }
96 |
97 | foreach ($userIds as $userId) {
98 | /** @var $user \AppBundle\Entity\User */
99 | $user = $this->userManager->findUserBy(['id'=>$userId]);
100 |
101 | if (!$user) {
102 | continue;
103 | }
104 |
105 | $user->addAccount($account);
106 | }
107 |
108 | $this->em->flush();
109 | }
110 |
111 | private function addFilesToAccount($fileIds, Account $account)
112 | {
113 | if (empty($fileIds)) {
114 | return false;
115 | }
116 |
117 | $fileIds = explode(',', $fileIds);
118 |
119 | if (empty($fileIds)) {
120 | return false;
121 | }
122 |
123 | foreach ($fileIds as $fileId) {
124 | /** @var $file \AppBundle\Entity\File */
125 | $file = $this->em->getRepository('AppBundle:File')->find($fileId);
126 |
127 | if (!$file) {
128 | continue;
129 | }
130 |
131 | $account->addFile($file);
132 | }
133 |
134 | $this->em->flush();
135 | }
136 | }
--------------------------------------------------------------------------------
/src/AppBundle/Features/user.feature:
--------------------------------------------------------------------------------
1 | Feature: Manage Users data via the RESTful API
2 |
3 | In order to offer the User resource via an hypermedia API
4 | As a client software developer
5 | I need to be able to retrieve, create, update, and delete JSON encoded User resources
6 |
7 |
8 | Background:
9 | Given there are Users with the following details:
10 | | uid | username | email | password |
11 | | u1 | peter | peter@test.com | testpass |
12 | | u2 | john | john@test.org | johnpass |
13 | # And there are Accounts with the following details:
14 | # | uid | name | users |
15 | # | a1 | account1 | u1 |
16 | And I am successfully logged in with username: "peter", and password: "testpass"
17 | And when consuming the endpoint I use the "headers/content-type" of "application/json"
18 |
19 |
20 | Scenario: User cannot GET a Collection of User objects
21 | When I send a "GET" request to "/users"
22 | Then the response code should be 405
23 |
24 |
25 | Scenario: User can GET their personal data by their unique ID
26 | When I send a "GET" request to "/users/u1"
27 | Then the response code should be 200
28 | And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
29 | And the response should contain json:
30 | """
31 | {
32 | "id": "u1",
33 | "email": "peter@test.com",
34 | "username": "peter"
35 | }
36 | """
37 |
38 |
39 | Scenario: User cannot GET a different User's personal data
40 | When I send a "GET" request to "/users/u2"
41 | Then the response code should be 403
42 |
43 |
44 | Scenario: User cannot determine if another User ID is active
45 | When I send a "GET" request to "/users/u100"
46 | Then the response code should be 403
47 |
48 |
49 | Scenario: User cannot POST to the Users collection
50 | When I send a "POST" request to "/users"
51 | Then the response code should be 405
52 |
53 |
54 | Scenario: User can PATCH to update their personal data
55 | When I send a "PATCH" request to "/users/u1" with body:
56 | """
57 | {
58 | "email": "peter@something-else.net",
59 | "current_password": "testpass"
60 | }
61 | """
62 | Then the response code should be 204
63 | And I send a "GET" request to "/users/u1"
64 | And the response should contain json:
65 | """
66 | {
67 | "id": "u1",
68 | "email": "peter@something-else.net",
69 | "username": "peter"
70 | }
71 | """
72 |
73 |
74 | Scenario: User cannot PATCH without a valid password
75 | When I send a "PATCH" request to "/users/u1" with body:
76 | """
77 | {
78 | "email": "peter@something-else.net",
79 | "current_password": "wrong-password"
80 | }
81 | """
82 | Then the response code should be 400
83 |
84 |
85 | Scenario: User cannot PATCH a different User's personal data
86 | When I send a "PATCH" request to "/users/u2"
87 | Then the response code should be 403
88 |
89 |
90 | Scenario: User cannot PATCH a none existent User
91 | When I send a "PATCH" request to "/users/u100"
92 | Then the response code should be 403
93 |
94 |
95 | Scenario: User cannot PUT to replace their personal data
96 | When I send a "PUT" request to "/users/u1"
97 | Then the response code should be 405
98 |
99 |
100 | Scenario: User cannot PUT a different User's personal data
101 | When I send a "PUT" request to "/users/u2"
102 | Then the response code should be 405
103 |
104 |
105 | Scenario: User cannot PUT a none existent User
106 | When I send a "PUT" request to "/users/u100"
107 | Then the response code should be 405
108 |
109 |
110 | Scenario: User cannot DELETE their personal data
111 | When I send a "DELETE" request to "/users/u1"
112 | Then the response code should be 405
113 |
114 |
115 | Scenario: User cannot DELETE a different User's personal data
116 | When I send a "DELETE" request to "/users/u2"
117 | Then the response code should be 405
118 |
119 |
120 | Scenario: User cannot DELETE a none existent User
121 | When I send a "DELETE" request to "/users/u100"
122 | Then the response code should be 405
--------------------------------------------------------------------------------
/src/AppBundle/Entity/User.php:
--------------------------------------------------------------------------------
1 | accounts = new ArrayCollection();
89 | }
90 |
91 | /**
92 | * @param AccountInterface $accountInterface
93 | * @return User
94 | */
95 | public function addAccount(AccountInterface $accountInterface)
96 | {
97 | if ( ! $this->hasAccount($accountInterface)) {
98 | $accountInterface->addUser($this);
99 | $this->accounts->add($accountInterface);
100 | }
101 |
102 | return $this;
103 | }
104 |
105 | /**
106 | * @param AccountInterface $accountInterface
107 | * @return User
108 | */
109 | public function removeAccount(AccountInterface $accountInterface)
110 | {
111 | if ($this->hasAccount($accountInterface)) {
112 | $accountInterface->removeUser($this);
113 | $this->accounts->removeElement($accountInterface);
114 | }
115 |
116 | return $this;
117 | }
118 |
119 | /**
120 | * @param AccountInterface $accountInterface
121 | * @return bool
122 | */
123 | public function hasAccount(AccountInterface $accountInterface)
124 | {
125 | return $this->accounts->contains($accountInterface);
126 | }
127 |
128 | /**
129 | * @return Collection
130 | */
131 | public function getAccounts()
132 | {
133 | return $this->accounts;
134 | }
135 |
136 | /**
137 | * @return string
138 | */
139 | function jsonSerialize()
140 | {
141 | return [
142 | 'id' => $this->id,
143 | 'username' => $this->username,
144 | 'accounts' => $this->accounts,
145 | ];
146 | }
147 | }
--------------------------------------------------------------------------------
/bin/symfony_requirements:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getPhpIniConfigPath();
9 |
10 | echo_title('Symfony Requirements Checker');
11 |
12 | echo '> PHP is using the following php.ini file:'.PHP_EOL;
13 | if ($iniPath) {
14 | echo_style('green', ' '.$iniPath);
15 | } else {
16 | echo_style('yellow', ' WARNING: No configuration file (php.ini) used by PHP!');
17 | }
18 |
19 | echo PHP_EOL.PHP_EOL;
20 |
21 | echo '> Checking Symfony requirements:'.PHP_EOL.' ';
22 |
23 | $messages = array();
24 | foreach ($symfonyRequirements->getRequirements() as $req) {
25 | if ($helpText = get_error_message($req, $lineSize)) {
26 | echo_style('red', 'E');
27 | $messages['error'][] = $helpText;
28 | } else {
29 | echo_style('green', '.');
30 | }
31 | }
32 |
33 | $checkPassed = empty($messages['error']);
34 |
35 | foreach ($symfonyRequirements->getRecommendations() as $req) {
36 | if ($helpText = get_error_message($req, $lineSize)) {
37 | echo_style('yellow', 'W');
38 | $messages['warning'][] = $helpText;
39 | } else {
40 | echo_style('green', '.');
41 | }
42 | }
43 |
44 | if ($checkPassed) {
45 | echo_block('success', 'OK', 'Your system is ready to run Symfony projects');
46 | } else {
47 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects');
48 |
49 | echo_title('Fix the following mandatory requirements', 'red');
50 |
51 | foreach ($messages['error'] as $helpText) {
52 | echo ' * '.$helpText.PHP_EOL;
53 | }
54 | }
55 |
56 | if (!empty($messages['warning'])) {
57 | echo_title('Optional recommendations to improve your setup', 'yellow');
58 |
59 | foreach ($messages['warning'] as $helpText) {
60 | echo ' * '.$helpText.PHP_EOL;
61 | }
62 | }
63 |
64 | echo PHP_EOL;
65 | echo_style('title', 'Note');
66 | echo ' The command console could use a different php.ini file'.PHP_EOL;
67 | echo_style('title', '~~~~');
68 | echo ' than the one used with your web server. To be on the'.PHP_EOL;
69 | echo ' safe side, please check the requirements from your web'.PHP_EOL;
70 | echo ' server using the ';
71 | echo_style('yellow', 'web/config.php');
72 | echo ' script.'.PHP_EOL;
73 | echo PHP_EOL;
74 |
75 | exit($checkPassed ? 0 : 1);
76 |
77 | function get_error_message(Requirement $requirement, $lineSize)
78 | {
79 | if ($requirement->isFulfilled()) {
80 | return;
81 | }
82 |
83 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL;
84 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL;
85 |
86 | return $errorMessage;
87 | }
88 |
89 | function echo_title($title, $style = null)
90 | {
91 | $style = $style ?: 'title';
92 |
93 | echo PHP_EOL;
94 | echo_style($style, $title.PHP_EOL);
95 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL);
96 | echo PHP_EOL;
97 | }
98 |
99 | function echo_style($style, $message)
100 | {
101 | // ANSI color codes
102 | $styles = array(
103 | 'reset' => "\033[0m",
104 | 'red' => "\033[31m",
105 | 'green' => "\033[32m",
106 | 'yellow' => "\033[33m",
107 | 'error' => "\033[37;41m",
108 | 'success' => "\033[37;42m",
109 | 'title' => "\033[34m",
110 | );
111 | $supports = has_color_support();
112 |
113 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : '');
114 | }
115 |
116 | function echo_block($style, $title, $message)
117 | {
118 | $message = ' '.trim($message).' ';
119 | $width = strlen($message);
120 |
121 | echo PHP_EOL.PHP_EOL;
122 |
123 | echo_style($style, str_repeat(' ', $width));
124 | echo PHP_EOL;
125 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT));
126 | echo PHP_EOL;
127 | echo_style($style, $message);
128 | echo PHP_EOL;
129 | echo_style($style, str_repeat(' ', $width));
130 | echo PHP_EOL;
131 | }
132 |
133 | function has_color_support()
134 | {
135 | static $support;
136 |
137 | if (null === $support) {
138 | if (DIRECTORY_SEPARATOR == '\\') {
139 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
140 | } else {
141 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT);
142 | }
143 | }
144 |
145 | return $support;
146 | }
147 |
--------------------------------------------------------------------------------
/app/Resources/views/default/index.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block body %}
4 |
5 |
6 |
7 |
Welcome to Symfony {{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | Your application is ready to start working on it at:
15 | {{ base_dir }}/
16 |
17 |
18 |
19 |
44 |
45 |
46 |
47 | {% endblock %}
48 |
49 | {% block stylesheets %}
50 |
76 | {% endblock %}
77 |
--------------------------------------------------------------------------------
/src/AppBundle/DTO/FileDTO.php:
--------------------------------------------------------------------------------
1 | $this->name
53 | ];
54 | }
55 |
56 | /**
57 | * @return string
58 | */
59 | public function getName()
60 | {
61 | return $this->name;
62 | }
63 |
64 | /**
65 | * @param string $name
66 | * @return $this
67 | */
68 | public function setName($name)
69 | {
70 | $this->name = $name;
71 |
72 | return $this;
73 | }
74 |
75 | /**
76 | * @return UploadedFileInterface
77 | */
78 | public function getUploadedFile()
79 | {
80 | if (!$this->uploadedFile instanceof UploadedFile) {
81 | return null;
82 | }
83 |
84 | return \AppBundle\Model\UploadedFile::createFromSymfonyUploadedFile($this->uploadedFile);
85 | }
86 |
87 | /**
88 | * @param UploadedFile $uploadedFile
89 | * @return $this
90 | */
91 | public function setUploadedFile(UploadedFile $uploadedFile)
92 | {
93 | $this->uploadedFile = $uploadedFile;
94 |
95 | return $this;
96 | }
97 |
98 |
99 | /**
100 | * @return mixed
101 | */
102 | public function getId()
103 | {
104 | throw new \RuntimeException('Should never be calling this on a File DTO');
105 | }
106 |
107 | /**
108 | * @return mixed
109 | */
110 | public function getOriginalFileName()
111 | {
112 | throw new \RuntimeException('Should never be calling this on a File DTO');
113 | }
114 |
115 | /**
116 | * @return mixed
117 | */
118 | public function getInternalFileName()
119 | {
120 | throw new \RuntimeException('Should never be calling this on a File DTO');
121 | }
122 |
123 | /**
124 | * @return mixed
125 | */
126 | public function getGuessedExtension()
127 | {
128 | throw new \RuntimeException('Should never be calling this on a File DTO');
129 | }
130 |
131 | /**
132 | * @return mixed
133 | */
134 | public function getFileSize()
135 | {
136 | throw new \RuntimeException('Should never be calling this on a File DTO');
137 | }
138 |
139 | /**
140 | * @return mixed
141 | */
142 | public function getDisplayedFileName()
143 | {
144 | throw new \RuntimeException('Should never be calling this on a File DTO');
145 | }
146 |
147 | /**
148 | * @param $newName
149 | * @return mixed
150 | */
151 | public function changeDisplayedFileName($newName)
152 | {
153 | throw new \RuntimeException('Should never be calling this on a File DTO');
154 | }
155 |
156 | /**
157 | * @param AccountInterface $accountInterface
158 | * @return mixed
159 | */
160 | public function addAccount(AccountInterface $accountInterface)
161 | {
162 | throw new \RuntimeException('Should never be calling this on a File DTO');
163 | }
164 |
165 | /**
166 | * @param AccountInterface $accountInterface
167 | * @return mixed
168 | */
169 | public function removeAccount(AccountInterface $accountInterface)
170 | {
171 | throw new \RuntimeException('Should never be calling this on a File DTO');
172 | }
173 |
174 | /**
175 | * @param AccountInterface $accountInterface
176 | * @return mixed
177 | */
178 | public function hasAccount(AccountInterface $accountInterface)
179 | {
180 | throw new \RuntimeException('Should never be calling this on a File DTO');
181 | }
182 |
183 | /**
184 | * @return mixed
185 | */
186 | public function getAccounts()
187 | {
188 | throw new \RuntimeException('Should never be calling this on a File DTO');
189 | }
190 |
191 | /**
192 | * @param UserInterface $user
193 | * @return mixed
194 | */
195 | public function belongsToUser(UserInterface $user)
196 | {
197 | throw new \RuntimeException('Should never be calling this on a File DTO');
198 | }
199 | }
--------------------------------------------------------------------------------
/app/config/config.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: parameters.yml }
3 | - { resource: security.yml }
4 | - { resource: services.yml }
5 |
6 | # Put parameters here that don't need to change on each machine where the app is deployed
7 | # http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
8 | parameters:
9 | locale: en
10 |
11 | framework:
12 | #esi: ~
13 | #translator: { fallbacks: ["%locale%"] }
14 | secret: "%secret%"
15 | router:
16 | resource: "%kernel.root_dir%/config/routing.yml"
17 | strict_requirements: ~
18 | form: ~
19 | csrf_protection: ~
20 | validation: { enable_annotations: true }
21 | #serializer: { enable_annotations: true }
22 | templating:
23 | engines: ['twig']
24 | #assets_version: SomeVersionScheme
25 | default_locale: "%locale%"
26 | trusted_hosts: ~
27 | trusted_proxies: ~
28 | session:
29 | # handler_id set to null will use default session handler from php.ini
30 | handler_id: ~
31 | save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
32 | fragments: ~
33 | http_method_override: true
34 | assets: ~
35 |
36 | # Twig Configuration
37 | twig:
38 | debug: "%kernel.debug%"
39 | strict_variables: "%kernel.debug%"
40 |
41 | # Doctrine Configuration
42 | doctrine:
43 | dbal:
44 | driver: pdo_mysql
45 | host: "%database_host%"
46 | port: "%database_port%"
47 | dbname: "%database_name%"
48 | user: "%database_user%"
49 | password: "%database_password%"
50 | charset: UTF8
51 | # if using pdo_sqlite as your database driver:
52 | # 1. add the path in parameters.yml
53 | # e.g. database_path: "%kernel.root_dir%/data/data.db3"
54 | # 2. Uncomment database_path in parameters.yml.dist
55 | # 3. Uncomment next line:
56 | # path: "%database_path%"
57 |
58 | orm:
59 | auto_generate_proxy_classes: "%kernel.debug%"
60 | naming_strategy: doctrine.orm.naming_strategy.underscore
61 | auto_mapping: true
62 |
63 | # Swiftmailer Configuration
64 | swiftmailer:
65 | transport: "%mailer_transport%"
66 | host: "%mailer_host%"
67 | username: "%mailer_user%"
68 | password: "%mailer_password%"
69 | spool: { type: memory }
70 |
71 |
72 | # Nelmio CORS
73 | nelmio_cors:
74 | defaults:
75 | allow_origin: ["%cors_allow_origin%"]
76 | allow_methods: ["POST", "PUT", "GET", "DELETE", "OPTIONS"]
77 | allow_headers: ["content-type", "authorization"]
78 | max_age: 3600
79 | paths:
80 | '^/': ~
81 |
82 | # Nelmio API Doc
83 | nelmio_api_doc:
84 | sandbox:
85 | accept_type: "application/json"
86 | body_format:
87 | formats: [ "json" ]
88 | default_format: "json"
89 | request_format:
90 | formats:
91 | json: "application/json"
92 |
93 |
94 | # FOS User Bundle
95 | fos_user:
96 | db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
97 | firewall_name: main
98 | user_class: AppBundle\Entity\User
99 | from_email:
100 | address: "%fos_user.from_email.address%"
101 | sender_name: "%fos_user.from_email.sender_name%"
102 |
103 |
104 | # FOS REST Bundle
105 | fos_rest:
106 | body_listener: true
107 | format_listener: true
108 | param_fetcher_listener: true
109 | view:
110 | view_response_listener: 'force'
111 | exception_wrapper_handler: null
112 | formats:
113 | jsonp: true
114 | json: true
115 | xml: false
116 | rss: false
117 | mime_types:
118 | json: ['application/json', 'application/x-json']
119 | jpg: 'image/jpeg'
120 | png: 'image/png'
121 | jsonp_handler: ~
122 | routing_loader:
123 | default_format: json
124 | include_format: false
125 | format_listener:
126 | rules:
127 | - { path: ^/, priorities: [ json, jsonp ], fallback_format: ~, prefer_extension: true }
128 | exception:
129 | enabled: true
130 |
131 |
132 | # Lexik JWT Auth
133 | lexik_jwt_authentication:
134 | private_key_path: %jwt_private_key_path%
135 | public_key_path: %jwt_public_key_path%
136 | pass_phrase: %jwt_key_pass_phrase%
137 | token_ttl: %jwt_token_ttl%
138 |
139 |
140 | # CSA Guzzle
141 | csa_guzzle:
142 | profiler: %kernel.debug%
143 |
144 |
145 | # JMS Serializer
146 | jms_serializer:
147 | metadata:
148 | directories:
149 | FOSUB:
150 | namespace_prefix: FOS\UserBundle
151 | path: %kernel.root_dir%/serializer/FOSUB
152 |
153 |
154 | # Oneup Flysystem
155 | oneup_flysystem:
156 | adapters:
157 | local_adapter:
158 | local:
159 | directory: %kernel.root_dir%/../uploads
160 | writeFlags: ~
161 | linkHandling: ~
162 | filesystems:
163 | local:
164 | adapter: local_adapter
165 | cache: ~
166 | alias: ~
167 | mount: ~
--------------------------------------------------------------------------------
/src/AppBundle/Entity/Account.php:
--------------------------------------------------------------------------------
1 | ")
34 | * @JMSSerializer\MaxDepth(2)
35 | * @JMSSerializer\Groups({"accounts_all"})
36 | */
37 | private $users;
38 |
39 | /**
40 | * @ORM\ManyToMany(targetEntity="File", inversedBy="accounts")
41 | * @ORM\JoinTable(name="accounts__files")
42 | *
43 | * @JMSSerializer\Expose
44 | * @JMSSerializer\Type("array")
45 | **/
46 | private $files;
47 |
48 | /**
49 | * @ORM\Column(type="string", name="name")
50 | * @JMSSerializer\Expose
51 | * @JMSSerializer\Groups({"accounts_all", "accounts_summary"})
52 | */
53 | private $name;
54 |
55 |
56 | public function __construct($accountName)
57 | {
58 | $this->name = $accountName;
59 | $this->users = new ArrayCollection();
60 | $this->files = new ArrayCollection();
61 | }
62 |
63 | public function getName()
64 | {
65 | return $this->name;
66 | }
67 |
68 | public function changeName($newName)
69 | {
70 | $this->name = (string) $newName;
71 |
72 | return $this;
73 | }
74 |
75 | public function getId()
76 | {
77 | return $this->id;
78 | }
79 |
80 | /**
81 | * @return mixed
82 | */
83 | public function getUsers()
84 | {
85 | return $this->users;
86 | }
87 |
88 | public function addUsers(array $users)
89 | {
90 | foreach ($users as $user) {
91 | $this->addUser($user);
92 | }
93 |
94 | return $this;
95 | }
96 |
97 | public function addUser(UserInterface $user)
98 | {
99 | if ( ! $this->isManagedBy($user)) {
100 | $this->users->add($user);
101 | }
102 |
103 | return $this;
104 | }
105 |
106 | public function isManagedBy(UserInterface $user)
107 | {
108 | return $this->users->contains($user);
109 | }
110 |
111 | public function removeUser(UserInterface $user)
112 | {
113 | if ($this->isManagedBy($user)) {
114 | $this->users->removeElement($user);
115 | }
116 |
117 | return $this;
118 | }
119 |
120 | public function removeAllUsers()
121 | {
122 | foreach ($this->users as $user) { /** @var UserInterface $user */
123 | $user->removeAccount($this);
124 | $this->removeUser($user);
125 | }
126 | }
127 |
128 |
129 | /**
130 | * @return ArrayCollection
131 | */
132 | public function getFiles()
133 | {
134 | return $this->files;
135 | }
136 |
137 | /**
138 | * @param array $files
139 | * @return $this
140 | */
141 | public function addFiles(array $files)
142 | {
143 | foreach ($files as $file) {
144 | $this->addFile($file);
145 | }
146 |
147 | return $this;
148 | }
149 |
150 | /**
151 | * @param FileInterface $file
152 | * @return $this
153 | */
154 | public function addFile(FileInterface $file)
155 | {
156 | if ( ! $this->usesFile($file)) {
157 | $file->addAccount($this);
158 | $this->files->add($file);
159 | }
160 |
161 | return $this;
162 | }
163 |
164 | /**
165 | * @param FileInterface $file
166 | * @return bool
167 | */
168 | public function usesFile(FileInterface $file)
169 | {
170 | return $this->files->contains($file);
171 | }
172 |
173 | /**
174 | * @param FileInterface $file
175 | * @return $this
176 | */
177 | public function removeFile(FileInterface $file)
178 | {
179 | if ($this->usesFile($file)) {
180 | $file->removeAccount($this);
181 | $this->files->removeElement($file);
182 | }
183 |
184 | return $this;
185 | }
186 |
187 | /**
188 | * @return $this
189 | */
190 | public function removeAllFiles()
191 | {
192 | foreach ($this->files as $file) { /** @var FileInterface $file */
193 | $this->removeFile($file);
194 | }
195 |
196 | return $this;
197 | }
198 |
199 |
200 |
201 | /**
202 | * @return mixed
203 | */
204 | function jsonSerialize()
205 | {
206 | return [
207 | 'id' => $this->id,
208 | 'name' => $this->name,
209 | 'users' => $this->users,
210 | 'files' => $this->files,
211 | ];
212 | }
213 |
214 | }
215 |
--------------------------------------------------------------------------------
/src/AppBundle/Handler/AccountHandler.php:
--------------------------------------------------------------------------------
1 | formHandler = $formHandler;
44 | $this->repository = $accountRepository;
45 | $this->factory = $accountFactory;
46 | $this->dataTransformer = $dataTransformer;
47 | }
48 |
49 | /**
50 | * @param int $id
51 | * @return mixed
52 | */
53 | public function get($id)
54 | {
55 | if ($id === null) {
56 | throw new BadRequestHttpException('An account ID was not specified.');
57 | }
58 |
59 | return $this->repository->findOneById($id);
60 | }
61 |
62 | /**
63 | * @param int $limit
64 | * @param int $offset
65 | * @return mixed
66 | */
67 | public function all($limit = 10, $offset = 0)
68 | {
69 | if ($this->user === null || ! $this->user instanceof UserInterface) {
70 | throw new \BadMethodCallException('Unable to find a User, did you remember to set one?');
71 | }
72 |
73 | return $this->repository->findAllForUser($this->user)->slice($offset, $limit);
74 | }
75 |
76 | /**
77 | * @param array $parameters
78 | * @param array $options
79 | * @return AccountInterface
80 | */
81 | public function post(array $parameters, array $options = [])
82 | {
83 | $accountDTO = $this->formHandler->handle(
84 | new AccountDTO(),
85 | $parameters,
86 | Request::METHOD_POST,
87 | $options
88 | );
89 |
90 | $account = $this->factory->createFromDTO($accountDTO);
91 |
92 | $this->repository->save($account);
93 |
94 | return $account;
95 | }
96 |
97 |
98 | /**
99 | * @param AccountInterface $account
100 | * @param array $parameters
101 | * @param array $options
102 | * @return mixed
103 | */
104 | public function patch($account, array $parameters, array $options = [])
105 | {
106 | $this->guardAccountImplementsInterface($account);
107 |
108 | /** @var AccountInterface $account */
109 | $accountDTO = $this->dataTransformer->convertToDTO($account);
110 |
111 | $accountDTO = $this->formHandler->handle(
112 | $accountDTO,
113 | $parameters,
114 | Request::METHOD_PATCH,
115 | $options
116 | );
117 |
118 | $this->repository->refresh($account);
119 |
120 | $account = $this->dataTransformer->updateFromDTO($account, $accountDTO);
121 |
122 | $this->repository->save($account);
123 |
124 | return $account;
125 | }
126 |
127 |
128 | /**
129 | * @param AccountInterface $account
130 | * @param array $parameters
131 | * @param array $options
132 | * @return mixed
133 | */
134 | public function put($account, array $parameters, array $options = [])
135 | {
136 | $this->guardAccountImplementsInterface($account);
137 |
138 | /** @var AccountInterface $account */
139 | $accountDTO = $this->dataTransformer->convertToDTO($account);
140 |
141 | $accountDTO = $this->formHandler->handle(
142 | $accountDTO,
143 | $parameters,
144 | Request::METHOD_PUT,
145 | $options
146 | );
147 |
148 | $this->repository->refresh($account);
149 |
150 | $account = $this->dataTransformer->updateFromDTO($account, $accountDTO);
151 |
152 | $this->repository->save($account);
153 |
154 | return $account;
155 | }
156 |
157 |
158 | /**
159 | * @param mixed $resource
160 | * @return bool
161 | */
162 | public function delete($resource)
163 | {
164 | $this->guardAccountImplementsInterface($resource);
165 |
166 | $this->repository->delete($resource);
167 |
168 | return true;
169 | }
170 |
171 | /**
172 | * @param UserInterface $user
173 | * @return $this
174 | */
175 | public function setUser($user)
176 | {
177 | $this->user = $user;
178 |
179 | return $this;
180 | }
181 |
182 | /**
183 | * @param $account
184 | */
185 | private function guardAccountImplementsInterface($account)
186 | {
187 | if (!$account instanceof AccountInterface) {
188 | throw new \InvalidArgumentException('Expected passed Account to implement AccountInterface');
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/AppBundle/Entity/File.php:
--------------------------------------------------------------------------------
1 | accounts = new ArrayCollection();
87 |
88 | $this->originalFileName = $originalFileName;
89 | $this->internalFileName = $internalFileName;
90 | $this->guessedExtension = $guessedExtension;
91 | $this->fileSize = $fileSize;
92 |
93 | $this->displayedFileName = $originalFileName;
94 | }
95 |
96 | /**
97 | * @return mixed
98 | */
99 | public function getId()
100 | {
101 | return $this->id;
102 | }
103 |
104 | /**
105 | * @return string
106 | */
107 | public function getOriginalFileName()
108 | {
109 | return $this->originalFileName;
110 | }
111 |
112 | /**
113 | * @return string
114 | */
115 | public function getInternalFileName()
116 | {
117 | return $this->internalFileName;
118 | }
119 |
120 | /**
121 | * @return string
122 | */
123 | public function getGuessedExtension()
124 | {
125 | return $this->guessedExtension;
126 | }
127 |
128 | /**
129 | * @return mixed
130 | */
131 | public function getDisplayedFileName()
132 | {
133 | return $this->displayedFileName;
134 | }
135 |
136 | /**
137 | * @return File
138 | */
139 | public function changeDisplayedFileName($newName)
140 | {
141 | $this->displayedFileName = $newName;
142 |
143 | return $this;
144 | }
145 |
146 | /**
147 | * @param AccountInterface $account
148 | * @return File
149 | */
150 | public function addAccount(AccountInterface $account)
151 | {
152 | if ( ! $this->hasAccount($account)) {
153 | $this->accounts->add($account);
154 | }
155 |
156 | return $this;
157 | }
158 |
159 | /**
160 | * @param AccountInterface $account
161 | * @return File
162 | */
163 | public function removeAccount(AccountInterface $account)
164 | {
165 | if ($this->hasAccount($account)) {
166 | $this->accounts->removeElement($account);
167 | }
168 |
169 | return $this;
170 | }
171 |
172 | /**
173 | * @param AccountInterface $account
174 | * @return bool
175 | */
176 | public function hasAccount(AccountInterface $account)
177 | {
178 | return $this->accounts->contains($account);
179 | }
180 |
181 | /**
182 | * @return Collection
183 | */
184 | public function getAccounts()
185 | {
186 | return $this->accounts;
187 | }
188 |
189 | /**
190 | * @return int
191 | */
192 | public function getFileSize()
193 | {
194 | return $this->fileSize;
195 | }
196 |
197 | /**
198 | * @param UserInterface $user
199 | * @return bool
200 | */
201 | public function belongsToUser(UserInterface $user)
202 | {
203 | return $this
204 | ->getAccounts()
205 | ->filter(function ($account) use ($user) { /** @var $account AccountInterface */
206 | return $account->isManagedBy($user);
207 | })
208 | ->count() > 0
209 | ;
210 | }
211 |
212 | /**
213 | * @return string
214 | */
215 | function jsonSerialize()
216 | {
217 | return [
218 | 'id' => $this->id,
219 | 'originalFileName' => $this->originalFileName,
220 | 'internalFileName' => $this->internalFileName,
221 | 'guessedExtension' => $this->guessedExtension,
222 | 'fileSize' => $this->fileSize,
223 | ];
224 | }
225 |
226 | }
--------------------------------------------------------------------------------
/src/AppBundle/Handler/FileHandler.php:
--------------------------------------------------------------------------------
1 | formHandler = $formHandler;
68 | $this->repository = $fileRepository;
69 | $this->factory = $fileFactory;
70 | $this->dataTransformer = $dataTransformer;
71 | $this->filesystem = $filesystem;
72 | $this->uploadFilesystem = $uploadFilesystem;
73 | }
74 |
75 | /**
76 | * @param AccountInterface $account
77 | * @return $this
78 | */
79 | public function setAccount(AccountInterface $account)
80 | {
81 | $this->account = $account;
82 |
83 | return $this;
84 | }
85 |
86 | /**
87 | * @return AccountInterface
88 | */
89 | public function getAccount()
90 | {
91 | if ( empty($this->account) || ! $this->account instanceof AccountInterface) {
92 | throw new \BadMethodCallException('Unable to find a valid Account. Did you forget to set it?');
93 | }
94 |
95 | return $this->account;
96 | }
97 |
98 | /**
99 | * @param int $id
100 | * @return mixed
101 | */
102 | public function get($id)
103 | {
104 | return $this->repository->findOneById($id);
105 | }
106 |
107 | /**
108 | * @param int $limit
109 | * @param int $offset
110 | * @return mixed
111 | */
112 | public function all($limit = 10, $offset = 0)
113 | {
114 | return $this->repository->findAllForAccount($this->getAccount())->slice($offset, $limit);
115 | }
116 |
117 | /**
118 | * @param array $parameters
119 | * @param array $options
120 | * @return FileInterface
121 | */
122 | public function post(array $parameters, array $options = [])
123 | {
124 | $account = $this->getAccount();
125 |
126 | $options = array_replace_recursive([
127 | 'validation_groups' => ['post'],
128 | 'has_file' => true,
129 | ], $options);
130 |
131 | $fileDTO = $this->formHandler->handle(
132 | new FileDTO(),
133 | $parameters,
134 | Request::METHOD_POST,
135 | $options
136 | ); /** @var $fileDTO FileDTO */
137 |
138 | $file = $this->factory->createFromUploadedFile($fileDTO->getUploadedFile());
139 | $file->changeDisplayedFileName($fileDTO->getName());
140 |
141 | $fileContents = $this->uploadFilesystem->getFileContentsFromPath($fileDTO->getUploadedFile()->getFilePath());
142 | $this->filesystem->put($file->getInternalFileName(), $fileContents);
143 |
144 | $account->addFile($file);
145 |
146 | $this->repository->save($file);
147 |
148 | return $file;
149 | }
150 |
151 |
152 | /**
153 | * @param FileInterface $file
154 | * @param array $parameters
155 | * @param array $options
156 | * @return mixed
157 | */
158 | public function patch($file, array $parameters, array $options = [])
159 | {
160 | $this->guardFileImplementsInterface($file);
161 |
162 | $options = array_replace_recursive([
163 | 'validation_groups' => ['patch'],
164 | 'has_file' => false,
165 | ], $options);
166 |
167 | $fileDTO = $this->formHandler->handle(
168 | new FileDTO(),
169 | $parameters,
170 | Request::METHOD_PATCH,
171 | $options
172 | ); /** @var $fileDTO FileDTO */
173 |
174 | $this->repository->refresh($file);
175 |
176 | $file = $this->dataTransformer->updateFromDTO($file, $fileDTO);
177 |
178 | $this->repository->save($file);
179 |
180 | return $file;
181 | }
182 |
183 |
184 | /**
185 | * @param FileInterface $file
186 | * @param array $parameters
187 | * @param array $options
188 | * @return mixed
189 | */
190 | public function put($file, array $parameters, array $options = [])
191 | {
192 | $this->guardFileImplementsInterface($file);
193 |
194 | $options = array_replace_recursive([
195 | 'validation_groups' => ['put'],
196 | 'has_file' => false,
197 | ], $options);
198 |
199 | $fileDTO = $this->formHandler->handle(
200 | new FileDTO(),
201 | $parameters,
202 | Request::METHOD_PUT,
203 | $options
204 | ); /** @var $fileDTO FileDTO */
205 |
206 | $this->repository->refresh($file);
207 |
208 | $file = $this->dataTransformer->updateFromDTO($file, $fileDTO);
209 |
210 | $this->repository->save($file);
211 |
212 | return $file;
213 | }
214 |
215 |
216 | /**
217 | * @param FileInterface $file
218 | * @return bool
219 | */
220 | public function delete($file)
221 | {
222 | $this->guardFileImplementsInterface($file);
223 |
224 | $isDeleted = $this->filesystem->delete($file->getInternalFileName());
225 |
226 | $this->repository->delete($file);
227 |
228 | return $isDeleted;
229 | }
230 |
231 | /**
232 | * This method is in place as class can't use a type hint due to interface
233 | * @param $file
234 | */
235 | private function guardFileImplementsInterface($file)
236 | {
237 | if (!$file instanceof FileInterface) {
238 | throw new \InvalidArgumentException('Expected passed File to implement FileInterface');
239 | }
240 | }
241 | }
--------------------------------------------------------------------------------
/src/AppBundle/Controller/AccountsController.php:
--------------------------------------------------------------------------------
1 | getAccountRepository()->findOneById($accountId);
48 | }
49 |
50 | /**
51 | * Gets a collection of the given User's Accounts.
52 | *
53 | * @ApiDoc(
54 | * output = "AppBundle\Entity\Account",
55 | * statusCodes = {
56 | * 200 = "Returned when successful",
57 | * 404 = "Returned when not found"
58 | * }
59 | * )
60 | *
61 | * @throws NotFoundHttpException when does not exist
62 | *
63 | * @Annotations\View(serializerGroups={
64 | * "accounts_all",
65 | * "users_summary"
66 | * })
67 | *
68 | * @return View
69 | */
70 | public function cgetAction()
71 | {
72 | $user = $this->getUser();
73 |
74 | return $this->getAccountRepository()->findAllForUser($user);
75 | }
76 |
77 |
78 | /**
79 | * Creates a new Account
80 | *
81 | * @ApiDoc(
82 | * input = "AppBundle\Form\Type\AccountFormType",
83 | * output = "AppBundle\Entity\Account",
84 | * statusCodes={
85 | * 201="Returned when a new Account has been successfully created",
86 | * 400="Returned when the posted data is invalid"
87 | * }
88 | * )
89 | *
90 | * @param Request $request
91 | * @return View
92 | */
93 | public function postAction(Request $request)
94 | {
95 | try {
96 |
97 | $account = $this->getHandler()->post($request->request->all());
98 |
99 | $routeOptions = [
100 | 'accountId' => $account->getId(),
101 | '_format' => $request->get('_format'),
102 | ];
103 |
104 | return $this->routeRedirectView('get_accounts', $routeOptions, Response::HTTP_CREATED);
105 |
106 | } catch (InvalidFormException $e) {
107 |
108 | return $e->getForm();
109 | }
110 | }
111 |
112 |
113 | /**
114 | * Update existing Account from the submitted data
115 | *
116 | * @ApiDoc(
117 | * resource = true,
118 | * input = "AppBundle\Form\AccountType",
119 | * output = "AppBundle\Entity\Account",
120 | * statusCodes = {
121 | * 204 = "Returned when successful",
122 | * 400 = "Returned when errors",
123 | * 404 = "Returned when not found"
124 | * }
125 | * )
126 | *
127 | * @param Request $request the request object
128 | * @param int $id the account id
129 | *
130 | * @return FormTypeInterface|RouteRedirectView
131 | *
132 | * @throws NotFoundHttpException when does not exist
133 | */
134 | public function patchAction(Request $request, $id)
135 | {
136 | $requestedAccount = $this->get('crv.repository.restricted_account_repository')->findOneById($id);
137 |
138 | try {
139 |
140 | $account = $this->getHandler()->patch(
141 | $requestedAccount,
142 | $request->request->all()
143 | );
144 |
145 | $routeOptions = [
146 | 'accountId' => $account->getId(),
147 | '_format' => $request->get('_format'),
148 | ];
149 |
150 | return $this->routeRedirectView('get_accounts', $routeOptions, Response::HTTP_NO_CONTENT);
151 |
152 | } catch (InvalidFormException $e) {
153 |
154 | return $e->getForm();
155 | }
156 | }
157 |
158 |
159 | /**
160 | * Replaces existing Account from the submitted data
161 | *
162 | * @ApiDoc(
163 | * resource = true,
164 | * input = "AppBundle\Form\AccountType",
165 | * output = "AppBundle\Entity\Account",
166 | * statusCodes = {
167 | * 204 = "Returned when successful",
168 | * 400 = "Returned when errors",
169 | * 404 = "Returned when not found"
170 | * }
171 | * )
172 | *
173 | * @param Request $request the request object
174 | * @param int $id the account id
175 | *
176 | * @return FormTypeInterface|RouteRedirectView
177 | *
178 | * @throws NotFoundHttpException when does not exist
179 | */
180 | public function putAction(Request $request, $id)
181 | {
182 | $requestedAccount = $this->get('crv.repository.restricted_account_repository')->findOneById($id);
183 |
184 | try {
185 |
186 | $account = $this->getHandler()->put(
187 | $requestedAccount,
188 | $request->request->all()
189 | );
190 |
191 | $routeOptions = [
192 | 'accountId' => $account->getId(),
193 | '_format' => $request->get('_format'),
194 | ];
195 |
196 | return $this->routeRedirectView('get_accounts', $routeOptions, Response::HTTP_NO_CONTENT);
197 |
198 | } catch (InvalidFormException $e) {
199 |
200 | return $e->getForm();
201 | }
202 | }
203 |
204 |
205 | /**
206 | * Deletes a specific Account by ID
207 | *
208 | * @ApiDoc(
209 | * description="Deletes an existing Account",
210 | * statusCodes={
211 | * 204="Returned when an existing Account has been successfully deleted",
212 | * 403="Returned when trying to delete a non existent Account"
213 | * }
214 | * )
215 | *
216 | * @param int $id the account id
217 | * @return View
218 | */
219 | public function deleteAction($id)
220 | {
221 | $requestedAccount = $this->get('crv.repository.restricted_account_repository')->findOneById($id);
222 |
223 | $this->getHandler()->delete($requestedAccount);
224 |
225 | return new View(null, Response::HTTP_NO_CONTENT);
226 | }
227 |
228 |
229 | /**
230 | * Returns the required handler for this controller
231 | *
232 | * @return \AppBundle\Handler\AccountHandler
233 | */
234 | private function getHandler()
235 | {
236 | return $this->get('crv.handler.restricted_account_handler');
237 | }
238 |
239 | /**
240 | * @return \AppBundle\Repository\Restricted\RestrictedAccountRepository
241 | */
242 | private function getAccountRepository()
243 | {
244 | return $this->get('crv.repository.restricted_account_repository');
245 | }
246 | }
--------------------------------------------------------------------------------
/src/AppBundle/Features/file.feature:
--------------------------------------------------------------------------------
1 | Feature: Manage uploaded Files through API
2 |
3 | In order to upload Files resources via an hypermedia API
4 | As a client software developer
5 | I need to be able to retrieve, create, update and delete Files resources.
6 |
7 |
8 | Background:
9 | Given there are Users with the following details:
10 | | uid | username | email | password |
11 | | u1 | peter | peter@test.com | testpass |
12 | | u2 | jonty | jonty@test.net | somepass |
13 | And there are files with the following details:
14 | | uid | originalFileName | internalFileName | guessedExtension | size | dummyFile |
15 | | f1 | some long file name.jpg | intfile1 | jpg | 100 | Image/pk140.jpg |
16 | | f2 | not_terrible.unk | intfile2 | bin | 20 | Image/phit200x100.png |
17 | | f3 | ok.png | intfile3 | png | 666 | |
18 | And there are Accounts with the following details:
19 | | uid | name | users | files |
20 | | a1 | account1 | u1 | f1,f3 |
21 | | a2 | account2 | u1 | f3 |
22 | | a3 | account3 | u2 | f2 |
23 | And I am successfully logged in with username: "peter", and password: "testpass"
24 | And when consuming the endpoint I use the "headers/content-type" of "application/json"
25 |
26 |
27 | Scenario: User can GET a Collection of their Files objects
28 | When I send a "GET" request to "/accounts/a1/files"
29 | Then the response code should be 200
30 | And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
31 | And the response should contain json:
32 | """
33 | [
34 | {
35 | "id": "f1",
36 | "originalFileName": "some long file name.jpg",
37 | "internalFileName": "intfile1",
38 | "guessedExtension": "jpg",
39 | "displayedFileName": "some long file name.jpg",
40 | "fileSize": 100
41 | },
42 | {
43 | "id": "f3",
44 | "originalFileName": "ok.png",
45 | "internalFileName": "intfile3",
46 | "guessedExtension": "png",
47 | "displayedFileName": "ok.png",
48 | "fileSize": 666
49 | }
50 | ]
51 | """
52 |
53 |
54 | Scenario: User can GET an individual File by ID
55 | When I send a "GET" request to "/accounts/a1/files/f1"
56 | Then the response code should be 200
57 | And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
58 | And the response should contain json:
59 | """
60 | {
61 | "id": "f1",
62 | "originalFileName": "some long file name.jpg",
63 | "internalFileName": "intfile1",
64 | "guessedExtension": "jpg",
65 | "displayedFileName": "some long file name.jpg",
66 | "fileSize": 100
67 | }
68 | """
69 |
70 |
71 | Scenario: User cannot access a valid File with the wrong Account ID
72 | When I send a "GET" request to "/accounts/a3/files/f2"
73 | Then the response code should be 403
74 |
75 |
76 | Scenario: User cannot determine if another File exists
77 | When I send a "GET" request to "/accounts/a3/files/f1000"
78 | Then the response code should be 403
79 |
80 |
81 | Scenario: User can add a new File
82 | When I send a multipart "POST" request to "/accounts/a1/files" with form data:
83 | | name | filePath |
84 | | a new file name | Image/pk140.jpg |
85 | Then the response code should be 201
86 | And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
87 | And the I follow the link in the Location response header
88 | And the response should contain json:
89 | """
90 | {
91 | "originalFileName": "pk140.jpg",
92 | "guessedExtension": "jpg",
93 | "displayedFileName": "a new file name",
94 | "fileSize": 8053
95 | }
96 | """
97 |
98 |
99 | Scenario: User cannot add a File to an Account they do not have access too
100 | When I send a multipart "POST" request to "/accounts/a3/files" with form data:
101 | | name | filePath |
102 | | a new file name | Image/pk140.jpg |
103 | Then the response code should be 403
104 |
105 |
106 | Scenario: User can PATCH to update the File metadata
107 | When I send a "PATCH" request to "/accounts/a1/files/f1" with body:
108 | """
109 | {
110 | "name": "a patched file name"
111 | }
112 | """
113 | Then the response code should be 204
114 | And I send a "GET" request to "/accounts/a1/files/f1"
115 | And the response should contain json:
116 | """
117 | {
118 | "id": "f1",
119 | "originalFileName": "some long file name.jpg",
120 | "internalFileName": "intfile1",
121 | "guessedExtension": "jpg",
122 | "displayedFileName": "a patched file name",
123 | "fileSize": 100
124 | }
125 | """
126 |
127 |
128 | Scenario: User cannot PATCH to update the File content
129 | When I send a "PATCH" request to "/accounts/a1/files/f1" with body:
130 | """
131 | {
132 | "file": "some new file"
133 | }
134 | """
135 | Then the response code should be 400
136 |
137 |
138 | Scenario: User cannot PATCH to update a File with which they are not authorised
139 | When I send a "PATCH" request to "/accounts/a3/files/f2"
140 | Then the response code should be 403
141 |
142 |
143 | Scenario: User cannot PATCH to update a none-existent File
144 | When I send a "PATCH" request to "/accounts/a1/files/madeup"
145 | Then the response code should be 403
146 |
147 |
148 | Scenario: User can PUT to replace their File metadata
149 | When I send a "PUT" request to "/accounts/a1/files/f1" with body:
150 | """
151 | {
152 | "name": "a put file name"
153 | }
154 | """
155 | Then the response code should be 204
156 | And the I follow the link in the Location response header
157 | And the response should contain json:
158 | """
159 | {
160 | "id": "f1",
161 | "originalFileName": "some long file name.jpg",
162 | "internalFileName": "intfile1",
163 | "guessedExtension": "jpg",
164 | "displayedFileName": "a put file name",
165 | "fileSize": 100
166 | }
167 | """
168 |
169 |
170 | Scenario: User cannot PUT to update the File content
171 | When I send a "PUT" request to "/accounts/a1/files/f1" with body:
172 | """
173 | {
174 | "file": "some new file"
175 | }
176 | """
177 | Then the response code should be 400
178 |
179 |
180 | Scenario: User cannot PUT to update a File with which they are not authorised
181 | When I send a "PUT" request to "/accounts/a3/files/f2"
182 | Then the response code should be 403
183 |
184 |
185 | Scenario: User cannot PUT to update a File that doesn't exist
186 | When I send a "PUT" request to "/accounts/a1/files/invalid-file-id"
187 | Then the response code should be 403
188 |
189 | @t
190 | Scenario: User can DELETE a File
191 | When I send a "DELETE" request to "/accounts/a1/files/f1"
192 | Then the response code should be 204
193 | And the "File" with id: f1 should have been deleted
194 | And the file with internal name: "intfile1" should have been deleted
195 |
196 |
197 | Scenario: User cannot DELETE a File they do not own
198 | When I send a "DELETE" request to "/accounts/a3/files/f2"
199 | Then the response code should be 403
200 | And the file with internal name: "intfile2" should not have been deleted
--------------------------------------------------------------------------------
/app/config/services.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | crv.user.entity: 'AppBundle\Entity\User'
3 |
4 | services:
5 |
6 | # -- DATA TRANSFORMERs --
7 | crv.data_transformer.account_data_transformer:
8 | class: AppBundle\DataTransformer\AccountDataTransformer
9 |
10 | crv.data_transformer.file_data_transformer:
11 | class: AppBundle\DataTransformer\FileDataTransformer
12 |
13 |
14 | # -- DOCTRINE ENTITY REPOSITORY --
15 | crv.doctrine_entity_repository.account:
16 | class: Doctrine\ORM\EntityRepository
17 | factory: ["@doctrine", getRepository]
18 | arguments:
19 | - AppBundle\Entity\Account
20 |
21 | crv.doctrine_entity_repository.file:
22 | class: Doctrine\ORM\EntityRepository
23 | factory: ["@doctrine", getRepository]
24 | arguments:
25 | - AppBundle\Entity\File
26 |
27 | crv.doctrine_entity_repository.user:
28 | class: Doctrine\ORM\EntityRepository
29 | factory: ["@doctrine", getRepository]
30 | arguments:
31 | - AppBundle\Entity\User
32 |
33 |
34 |
35 | # -- EVENTS --
36 | crv.event.listener.content_type_listener:
37 | class: AppBundle\Event\Listener\ContentTypeListener
38 | tags:
39 | - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
40 |
41 |
42 |
43 | # -- FACTORY --
44 | crv.factory.account_factory:
45 | class: AppBundle\Factory\AccountFactory
46 |
47 | crv.factory.file_factory:
48 | class: AppBundle\Factory\FileFactory
49 |
50 |
51 | # -- FORM --
52 | crv.form.type.account:
53 | class: AppBundle\Form\Type\AccountType
54 | arguments:
55 | - "@crv.repository.doctrine_user_repository"
56 | tags:
57 | - { name: form.type, alias: account_form }
58 |
59 | crv.form.type.file:
60 | class: AppBundle\Form\Type\FileType
61 | tags:
62 | - { name: form.type, alias: file_form }
63 |
64 | crv.form.type.user:
65 | class: AppBundle\Form\Type\UserType
66 | arguments:
67 | - "%crv.user.entity%"
68 | tags:
69 | - { name: form.type, alias: user_form }
70 |
71 |
72 | crv.form.type.restricted_account:
73 | class: AppBundle\Form\Type\AccountType
74 | arguments:
75 | - "@crv.repository.restricted_user_repository"
76 | tags:
77 | - { name: form.type, alias: restricted_account_form }
78 |
79 | crv.form.type.restricted_file:
80 | class: AppBundle\Form\Type\FileType
81 | tags:
82 | - { name: form.type, alias: restricted_file_form }
83 |
84 | crv.form.type.restricted_user:
85 | class: AppBundle\Form\Type\UserType
86 | arguments:
87 | - "%crv.user.entity%"
88 | tags:
89 | - { name: form.type, alias: restricted_user_form }
90 |
91 |
92 | # -- FORM HANDLER --
93 | crv.form.handler.account_form_handler:
94 | class: AppBundle\Form\Handler\FormHandler
95 | arguments:
96 | - "@form.factory"
97 | - "@crv.form.type.account"
98 |
99 | crv.form.handler.file_form_handler:
100 | class: AppBundle\Form\Handler\FormHandler
101 | arguments:
102 | - "@form.factory"
103 | - "@crv.form.type.file"
104 |
105 | crv.form.handler.user_form_handler:
106 | class: AppBundle\Form\Handler\FormHandler
107 | arguments:
108 | - "@form.factory"
109 | - "@crv.form.type.user"
110 |
111 |
112 | crv.form.handler.restricted_account_form_handler:
113 | class: AppBundle\Form\Handler\FormHandler
114 | arguments:
115 | - "@form.factory"
116 | - "@crv.form.type.restricted_account"
117 |
118 | crv.form.handler.restricted_file_form_handler:
119 | class: AppBundle\Form\Handler\FormHandler
120 | arguments:
121 | - "@form.factory"
122 | - "@crv.form.type.restricted_file"
123 |
124 | crv.form.handler.restricted_user_form_handler:
125 | class: AppBundle\Form\Handler\FormHandler
126 | arguments:
127 | - "@form.factory"
128 | - "@crv.form.type.restricted_user"
129 |
130 |
131 | # -- HANDLER --
132 | crv.handler.restricted_account_handler:
133 | class: AppBundle\Handler\AccountHandler
134 | arguments:
135 | - "@crv.form.handler.restricted_account_form_handler"
136 | - "@crv.data_transformer.account_data_transformer"
137 | - "@crv.repository.restricted_account_repository"
138 | - "@crv.factory.account_factory"
139 |
140 | crv.handler.restricted_file_handler:
141 | class: AppBundle\Handler\FileHandler
142 | arguments:
143 | - "@crv.form.handler.restricted_file_form_handler"
144 | - "@crv.data_transformer.file_data_transformer"
145 | - "@crv.repository.restricted_file_repository"
146 | - "@crv.factory.file_factory"
147 | - "@oneup_flysystem.local_filesystem"
148 | - "@crv.utility.upload_file_system"
149 |
150 | crv.handler.restricted_user_handler:
151 | class: AppBundle\Handler\UserHandler
152 | arguments:
153 | - "@crv.form.handler.restricted_user_form_handler"
154 | - "@crv.repository.restricted_user_repository"
155 |
156 |
157 |
158 | # -- REPOSITORY --
159 | crv.repository.common_doctrine_repository:
160 | class: AppBundle\Repository\Doctrine\CommonDoctrineRepository
161 | arguments:
162 | - "@doctrine.orm.entity_manager"
163 |
164 | crv.repository.doctrine_account_repository:
165 | class: AppBundle\Repository\Doctrine\DoctrineAccountRepository
166 | arguments:
167 | - "@crv.repository.common_doctrine_repository"
168 | - "@crv.doctrine_entity_repository.account"
169 |
170 | crv.repository.doctrine_file_repository:
171 | class: AppBundle\Repository\Doctrine\DoctrineFileRepository
172 | arguments:
173 | - "@crv.repository.common_doctrine_repository"
174 | - "@crv.doctrine_entity_repository.file"
175 |
176 | crv.repository.doctrine_user_repository:
177 | class: AppBundle\Repository\Doctrine\DoctrineUserRepository
178 | arguments:
179 | - "@crv.repository.common_doctrine_repository"
180 | - "@doctrine.orm.entity_manager"
181 |
182 |
183 | crv.repository.restricted_account_repository:
184 | class: AppBundle\Repository\Restricted\RestrictedAccountRepository
185 | arguments:
186 | - "@crv.repository.doctrine_account_repository"
187 | - "@security.authorization_checker"
188 |
189 | crv.repository.restricted_file_repository:
190 | class: AppBundle\Repository\Restricted\RestrictedFileRepository
191 | arguments:
192 | - "@crv.repository.doctrine_file_repository"
193 | - "@security.authorization_checker"
194 |
195 | crv.repository.restricted_user_repository:
196 | class: AppBundle\Repository\Restricted\RestrictedUserRepository
197 | arguments:
198 | - "@crv.repository.doctrine_user_repository"
199 | - "@security.authorization_checker"
200 |
201 |
202 |
203 | # # -- MANAGERS --
204 | # crv.manager.user_manager:
205 | # class: AppBundle\Model\UserManager
206 | # arguments:
207 | # - "@fos_user.user_manager"
208 |
209 |
210 |
211 | # -- SECURITY ---
212 | crv.security.authorization.voter.account_voter:
213 | class: AppBundle\Security\Authorization\Voter\AccountVoter
214 | public: false
215 | tags:
216 | - { name: security.voter }
217 |
218 | crv.security.authorization.voter.file_voter:
219 | class: AppBundle\Security\Authorization\Voter\FileVoter
220 | public: false
221 | tags:
222 | - { name: security.voter }
223 |
224 | crv.security.authorization.voter.user_voter:
225 | class: AppBundle\Security\Authorization\Voter\UserVoter
226 | public: false
227 | tags:
228 | - { name: security.voter }
229 |
230 |
231 |
232 | # -- UTILITY --
233 | crv.utility.upload_file_system:
234 | class: AppBundle\Util\UploadFilesystem
--------------------------------------------------------------------------------
/src/AppBundle/Controller/FilesController.php:
--------------------------------------------------------------------------------
1 | getFileHandler()->get($fileId);
47 | }
48 |
49 | /**
50 | * Gets a collection of the given User's Files.
51 | *
52 | * @ApiDoc(
53 | * output = "AppBundle\Entity\File",
54 | * statusCodes = {
55 | * 200 = "Returned when successful",
56 | * 404 = "Returned when not found"
57 | * }
58 | * )
59 | *
60 | * @param int $accountId the Account id
61 | *
62 | * @Annotations\View(serializerGroups={ "files_all", "accounts_summary" })
63 | *
64 | * @throws NotFoundHttpException when does not exist
65 | *
66 | * @return View
67 | */
68 | public function cgetAction($accountId)
69 | {
70 | $account = $this->getAccountHandler()->get($accountId);
71 |
72 | return $this
73 | ->getFileHandler()
74 | ->setAccount($account)
75 | ->all()
76 | ;
77 | }
78 |
79 |
80 | /**
81 | * Creates a new File
82 | *
83 | * @ApiDoc(
84 | * input = "AppBundle\Form\Type\FileType",
85 | * output = "AppBundle\Entity\File",
86 | * statusCodes={
87 | * 201="Returned when a new File has been successfully created",
88 | * 400="Returned when the posted data is invalid"
89 | * }
90 | * )
91 | *
92 | * @param Request $request
93 | * @param int $accountId the account id
94 | *
95 | * @return View
96 | */
97 | public function postAction(Request $request, $accountId)
98 | {
99 | $this->getFileHandler()->setAccount(
100 | $this->getAccountHandler()->get($accountId)
101 | );
102 |
103 | $parameters = array_replace_recursive(
104 | $request->request->all(),
105 | $request->files->all()
106 | );
107 |
108 | try {
109 | $file = $this->getFileHandler()->post($parameters);
110 | } catch (InvalidFormException $e) {
111 |
112 | return $e->getForm();
113 | }
114 |
115 | $routeOptions = [
116 | 'accountId' => $accountId,
117 | 'fileId' => $file->getId(),
118 | '_format' => $request->get('_format'),
119 | ];
120 |
121 | return $this->routeRedirectView('get_accounts_files', $routeOptions, Response::HTTP_CREATED);
122 | }
123 |
124 |
125 | /**
126 | * Update existing File from the submitted data
127 | *
128 | * @ApiDoc(
129 | * resource = true,
130 | * input = "AppBundle\Form\FileType",
131 | * output = "AppBundle\Entity\File",
132 | * statusCodes = {
133 | * 204 = "Returned when successful",
134 | * 400 = "Returned when errors",
135 | * 404 = "Returned when not found"
136 | * }
137 | * )
138 | *
139 | * @param Request $request the request object
140 | * @param int $accountId the account id
141 | * @param int $fileId the file id
142 | *
143 | * @return FormTypeInterface|RouteRedirectView
144 | *
145 | * @throws NotFoundHttpException when does not exist
146 | */
147 | public function patchAction(Request $request, $accountId, $fileId)
148 | {
149 | $this->getFileHandler()->setAccount(
150 | $this->getAccountHandler()->get($accountId)
151 | );
152 |
153 | $file = $this->getFileHandler()->get($fileId);
154 |
155 | $parameters = array_replace_recursive(
156 | $request->request->all(),
157 | $request->files->all()
158 | );
159 |
160 | try {
161 | $file = $this->getFileHandler()->patch($file, $parameters);
162 | } catch (InvalidFormException $e) {
163 |
164 | return $e->getForm();
165 | }
166 |
167 | $routeOptions = [
168 | 'accountId' => $fileId,
169 | 'fileId' => $file->getId(),
170 | '_format' => $request->get('_format'),
171 | ];
172 |
173 | return $this->routeRedirectView('get_accounts_files', $routeOptions, Response::HTTP_NO_CONTENT);
174 | }
175 |
176 |
177 | /**
178 | * Replaces existing File from the submitted data
179 | *
180 | * @ApiDoc(
181 | * resource = true,
182 | * input = "AppBundle\Form\FileType",
183 | * output = "AppBundle\Entity\File",
184 | * statusCodes = {
185 | * 204 = "Returned when successful",
186 | * 400 = "Returned when errors",
187 | * 404 = "Returned when not found"
188 | * }
189 | * )
190 | *
191 | * @param Request $request the request object
192 | * @param int $accountId the account id
193 | * @param int $fileId the file id
194 | *
195 | * @return FormTypeInterface|RouteRedirectView
196 | *
197 | * @throws NotFoundHttpException when does not exist
198 | */
199 | public function putAction(Request $request, $accountId, $fileId)
200 | {
201 | $this->getFileHandler()->setAccount(
202 | $this->getAccountHandler()->get($accountId)
203 | );
204 |
205 | $file = $this->getFileHandler()->get($fileId);
206 |
207 | $parameters = array_replace_recursive(
208 | $request->request->all(),
209 | $request->files->all()
210 | );
211 |
212 | try {
213 | $file = $this->getFileHandler()->put($file, $parameters);
214 | } catch (InvalidFormException $e) {
215 |
216 | return $e->getForm();
217 | }
218 |
219 | $routeOptions = [
220 | 'accountId' => $fileId,
221 | 'fileId' => $file->getId(),
222 | '_format' => $request->get('_format'),
223 | ];
224 |
225 | return $this->routeRedirectView('get_accounts_files', $routeOptions, Response::HTTP_NO_CONTENT);
226 | }
227 |
228 |
229 | /**
230 | * Deletes a specific File by ID
231 | *
232 | * @ApiDoc(
233 | * description="Deletes an existing File",
234 | * statusCodes={
235 | * 204 = "Returned when an existing File has been successfully deleted",
236 | * 403 = "Returned when trying to delete a non existent File"
237 | * }
238 | * )
239 | *
240 | * @param int $accountId the account id
241 | * @param int $fileId the file id
242 | *
243 | * @return View
244 | *
245 | * @throws NotFoundHttpException when does not exist
246 | */
247 | public function deleteAction($accountId, $fileId)
248 | {
249 | $file = $this->getFileHandler()->get($fileId);
250 |
251 | $this->getFileHandler()->delete($file);
252 |
253 | return new View(null, Response::HTTP_NO_CONTENT);
254 | }
255 |
256 |
257 | /**
258 | * @return \AppBundle\Handler\FileHandler
259 | */
260 | private function getFileHandler()
261 | {
262 | return $this->get('crv.handler.restricted_file_handler');
263 | }
264 |
265 |
266 | /**
267 | * @return \AppBundle\Handler\AccountHandler
268 | */
269 | private function getAccountHandler()
270 | {
271 | return $this->get('crv.handler.restricted_account_handler');
272 | }
273 | }
--------------------------------------------------------------------------------