diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2781a92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea +.php_cs.cache +cache +phpunit +vendor +composer.lock +var diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2a64f4b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "vagrant-php"] + path = vagrant-php + url = https://github.com/vagrant-php/ubuntu.git + branch = 16.04 diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..3433811 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,18 @@ +build: + tests: + override: + - + command: 'vendor/bin/phpunit --coverage-clover=coverage/clover.xml' + coverage: + file: 'coverage/clover.xml' + format: 'php-clover' + +checks: + php: true + +coding_style: + php: {} + +filter: + excluded_paths: + - 'tests/*' diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..43b05f4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: php + +php: + - 7.0 + - 7.1 + +services: + - mysql + +matrix: + include: + - php: 7.0 + env: dependencies=lowest + - php: 7.0 + env: dependencies=highest + - php: 7.1 + env: dependencies=lowest + - php: 7.1 + env: dependencies=highest + +before_script: + - echo "USE mysql;\nUPDATE user SET password=PASSWORD('root') WHERE user='root';\nFLUSH PRIVILEGES;\n" | mysql -u root + - composer self-update -q + - if [ -z "$dependencies" ]; then composer install; fi; + - if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest -n; fi; + - if [ "$dependencies" = "highest" ]; then composer update -n; fi; + +script: vendor/bin/phpunit --coverage-text --verbose diff --git a/README.md b/README.md new file mode 100644 index 0000000..12b493e --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# chubbyphp/chubbyphp-api-slim-skeleton + +[![Build Status](https://api.travis-ci.org/chubbyphp/chubbyphp-api-slim-skeleton.png?branch=master)](https://travis-ci.org/chubbyphp/chubbyphp-api-slim-skeleton) +[![Total Downloads](https://poser.pugx.org/chubbyphp/chubbyphp-api-slim-skeleton/downloads.png)](https://packagist.org/packages/chubbyphp/chubbyphp-api-slim-skeleton) +[![Latest Stable Version](https://poser.pugx.org/chubbyphp/chubbyphp-api-slim-skeleton/v/stable.png)](https://packagist.org/packages/chubbyphp/chubbyphp-api-slim-skeleton) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/chubbyphp/chubbyphp-api-slim-skeleton/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/chubbyphp/chubbyphp-api-slim-skeleton/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/chubbyphp/chubbyphp-api-slim-skeleton/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/chubbyphp/chubbyphp-api-slim-skeleton/?branch=master) + +## Description + +A slim 3 skeleton to build web apis. + +## Requirements + + * php: ~7.0 + * [chubbyphp/chubbyphp-api-http][1]: ~1.0@beta + * [chubbyphp/chubbyphp-deserialization][2]: ~1.1 + * [chubbyphp/chubbyphp-deserialization-model][3]: ~1.0 + * [chubbyphp/chubbyphp-lazy][4]: ~1.1 + * [chubbyphp/chubbyphp-model][5]: ~3.0 + * [chubbyphp/chubbyphp-model-doctrine-dbal][6]: ~1.0 + * [chubbyphp/chubbyphp-serialization][7]: ^1.0.1 + * [chubbyphp/chubbyphp-translation][8]: ^1.1.2 + * [chubbyphp/chubbyphp-validation][9]: ~2.1 + * [chubbyphp/chubbyphp-validation-model][10]: ~1.0 + * [monolog/monolog][11]: ~1.21 + * [ramsey/uuid][12]: ~3.5 + * [silex/providers][13]: ^2.0.3 + * [slim/slim][14]: ~3.5 + * [symfony/console][15]: ~3.1 + * [symfony/yaml][16]: ~3.1 + * [willdurand/negotiation][17]: ^2.3 + +## Installation + +### With vagrant-php + +#### Install `create-slim3-project` command + +[create-slim3-project][18] + +#### Create project + +```{.sh} +create-slim3-project --name=myproject --vagrantIp=10.15.10.15 +``` + +### With php on host + +```{.sh} +composer create-project chubbyphp/chubbyphp-api-slim-skeleton myproject dev-master --prefer-dist +``` + +## Setup + +### Create database + +```{.sh} +bin/console chubbyphp:model:dbal:database:create +``` + +### Create / Update schema + +```{.sh} +bin/console chubbyphp:model:dbal:database:schema:update --dump --force +``` + +[1]: https://github.com/chubbyphp/chubbyphp-api-http +[2]: https://github.com/chubbyphp/chubbyphp-deserialization +[3]: https://github.com/chubbyphp/chubbyphp-deserialization-model +[4]: https://github.com/chubbyphp/chubbyphp-lazy +[5]: https://github.com/chubbyphp/chubbyphp-model +[6]: https://github.com/chubbyphp/chubbyphp-model-doctrine-dbal +[7]: https://github.com/chubbyphp/chubbyphp-serialization +[8]: https://github.com/chubbyphp/chubbyphp-translation +[9]: https://github.com/chubbyphp/chubbyphp-validation +[10]: https://github.com/chubbyphp/chubbyphp-validation-model +[11]: https://github.com/Seldaek/monolog +[12]: https://github.com/ramsey/uuid +[13]: https://github.com/silexphp/Silex-Providers +[14]: https://github.com/slimphp/Slim +[15]: https://github.com/symfony/console +[16]: https://github.com/symfony/yaml +[17]: https://github.com/willdurand/negotiation +[18]: https://github.com/vagrant-php/create-slim3-project diff --git a/app/ApiHttp/ResponseFactory.php b/app/ApiHttp/ResponseFactory.php new file mode 100644 index 0000000..efe587a --- /dev/null +++ b/app/ApiHttp/ResponseFactory.php @@ -0,0 +1,22 @@ +responseManager = $responseManager; + } + + /** + * @param Request $request + * + * @return Response + */ + public function __invoke(Request $request) + { + return $this->responseManager->createResponse($request, 200, new Index()); + } +} diff --git a/app/Controller/Sample/SampleCreateController.php b/app/Controller/Sample/SampleCreateController.php new file mode 100644 index 0000000..6c07925 --- /dev/null +++ b/app/Controller/Sample/SampleCreateController.php @@ -0,0 +1,104 @@ +defaultLanguage = $defaultLanguage; + $this->errorManager = $errorManager; + $this->repository = $repository; + $this->requestManager = $requestManager; + $this->responseManager = $responseManager; + $this->validator = $validator; + } + + /** + * @param Request $request + * + * @return Response + */ + public function __invoke(Request $request): Response + { + /** @var Sample $sample */ + $sample = $this->requestManager->getDataFromRequestBody($request, Sample::class); + + if (null === $sample) { + return $this->responseManager->createResponse($request, 415); + } + + if ([] !== $errors = $this->validator->validateObject($sample)) { + return $this->responseManager->createResponse( + $request, + 422, + $this->errorManager->createByValidationErrors( + $errors, + $this->requestManager->getAcceptLanguage($request, $this->defaultLanguage), + Error::SCOPE_BODY, + 'sample' + ) + ); + } + + $this->repository->persist($sample); + + return $this->responseManager->createResponse($request, 201, $sample); + } +} diff --git a/app/Controller/Sample/SampleDeleteController.php b/app/Controller/Sample/SampleDeleteController.php new file mode 100644 index 0000000..c2d3ad6 --- /dev/null +++ b/app/Controller/Sample/SampleDeleteController.php @@ -0,0 +1,70 @@ +errorManager = $errorManager; + $this->repository = $repository; + $this->responseManager = $responseManager; + } + + /** + * @param Request $request + * + * @return Response + */ + public function __invoke(Request $request): Response + { + $id = $request->getAttribute('id'); + + /** @var Sample $sample */ + $sample = $this->repository->find($id); + + if (null === $sample) { + return $this->responseManager->createResponse( + $request, + 404, + $this->errorManager->createByMissingModel('sample', ['id' => $id]) + ); + } + + $this->repository->remove($sample); + + return $this->responseManager->createResponse($request, 200); + } +} diff --git a/app/Controller/Sample/SampleReadController.php b/app/Controller/Sample/SampleReadController.php new file mode 100644 index 0000000..a3203be --- /dev/null +++ b/app/Controller/Sample/SampleReadController.php @@ -0,0 +1,68 @@ +errorManager = $errorManager; + $this->repository = $repository; + $this->responseManager = $responseManager; + } + + /** + * @param Request $request + * + * @return Response + */ + public function __invoke(Request $request): Response + { + $id = $request->getAttribute('id'); + + /** @var Sample $sample */ + $sample = $this->repository->find($id); + + if (null === $sample) { + return $this->responseManager->createResponse( + $request, + 404, + $this->errorManager->createByMissingModel('sample', ['id' => $id]) + ); + } + + return $this->responseManager->createResponse($request, 200, $sample); + } +} diff --git a/app/Controller/Sample/SampleSearchController.php b/app/Controller/Sample/SampleSearchController.php new file mode 100644 index 0000000..359e328 --- /dev/null +++ b/app/Controller/Sample/SampleSearchController.php @@ -0,0 +1,102 @@ +defaultLanguage = $defaultLanguage; + $this->errorManager = $errorManager; + $this->repository = $repository; + $this->requestManager = $requestManager; + $this->responseManager = $responseManager; + $this->validator = $validator; + } + + /** + * @param Request $request + * + * @return Response + */ + public function __invoke(Request $request): Response + { + /** @var SampleSearch $sampleSearch */ + $sampleSearch = $this->requestManager->getDataFromRequestQuery($request, SampleSearch::class); + + if ([] !== $errors = $this->validator->validateObject($sampleSearch)) { + return $this->responseManager->createResponse( + $request, + 400, + $this->errorManager->createByValidationErrors( + $errors, + $this->requestManager->getAcceptLanguage($request, $this->defaultLanguage), + Error::SCOPE_BODY, + Sample::class + ) + ); + } + + /** @var SampleSearch $sampleSearch */ + $sampleSearch = $this->repository->search($sampleSearch); + + return $this->responseManager->createResponse($request, 200, $sampleSearch); + } +} diff --git a/app/Controller/Sample/SampleUpdateController.php b/app/Controller/Sample/SampleUpdateController.php new file mode 100644 index 0000000..c38354b --- /dev/null +++ b/app/Controller/Sample/SampleUpdateController.php @@ -0,0 +1,117 @@ +defaultLanguage = $defaultLanguage; + $this->errorManager = $errorManager; + $this->repository = $repository; + $this->requestManager = $requestManager; + $this->responseManager = $responseManager; + $this->validator = $validator; + } + + /** + * @param Request $request + * + * @return Response + */ + public function __invoke(Request $request): Response + { + $id = $request->getAttribute('id'); + + /** @var Sample $sample */ + $sample = $this->repository->find($id); + + if (null === $sample) { + return $this->responseManager->createResponse( + $request, + 404, + $this->errorManager->createByMissingModel('sample', ['id' => $id]) + ); + } + + /** @var Sample $sample */ + $sample = $this->requestManager->getDataFromRequestBody($request, $sample); + + if (null === $sample) { + return $this->responseManager->createResponse($request, 415); + } + + if ([] !== $errors = $this->validator->validateObject($sample)) { + return $this->responseManager->createResponse( + $request, + 422, + $this->errorManager->createByValidationErrors( + $errors, + $this->requestManager->getAcceptLanguage($request, $this->defaultLanguage), + Error::SCOPE_BODY, + 'sample' + ) + ); + } + + $this->repository->persist($sample); + + return $this->responseManager->createResponse($request, 200, $sample); + } +} diff --git a/app/Deserialization/SampleMapping.php b/app/Deserialization/SampleMapping.php new file mode 100644 index 0000000..94f9960 --- /dev/null +++ b/app/Deserialization/SampleMapping.php @@ -0,0 +1,37 @@ +scope = $scope; + $this->key = $key; + $this->detail = $detail; + $this->reference = $reference; + $this->arguments = $arguments; + } + + /** + * @return null|string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return string|null + */ + public function getDetail() + { + return $this->detail; + } + + /** + * @return string|null + */ + public function getReference() + { + return $this->reference; + } + + /** + * @return array|null + */ + public function getArguments() + { + return $this->arguments; + } +} diff --git a/app/Error/ErrorManager.php b/app/Error/ErrorManager.php new file mode 100644 index 0000000..fcd9dd6 --- /dev/null +++ b/app/Error/ErrorManager.php @@ -0,0 +1,64 @@ +translator = $translator; + } + + /** + * @param string $type + * @param array $arguments + * + * @return Error + */ + public function createByMissingModel(string $type, array $arguments): Error + { + return new Error( + Error::SCOPE_RESOURCE, + 'missing.model', + 'there wished model does not exist', + $type, + $arguments + ); + } + + /** + * @param array $errors + * @param string $locale + * @param string $scope + * @param string $type + * + * @return Error + */ + public function createByValidationErrors(array $errors, string $locale, string $scope, string $type): Error + { + $nestedErrorMessage = new NestedErrorMessages($errors, function (string $key, array $arguments) use ($locale) { + return $this->translator->translate($locale, $key, $arguments); + }); + + return new Error( + $scope, + 'validation.errors', + 'there are validation errors while validating the model', + $type, + $nestedErrorMessage->getMessages() + ); + } +} diff --git a/app/Model/Sample.php b/app/Model/Sample.php new file mode 100644 index 0000000..20a1e01 --- /dev/null +++ b/app/Model/Sample.php @@ -0,0 +1,64 @@ +id = $id ?? (string) Uuid::uuid4(); + + return $self; + } + + /** + * @param array $data + * + * @return self|ModelInterface + */ + public static function fromPersistence(array $data): ModelInterface + { + $self = new self(); + $self->id = $data['id']; + + return $self; + } + + /** + * @return array + */ + public function toPersistence(): array + { + return [ + 'id' => $this->id, + ]; + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } +} diff --git a/app/Provider/ConsoleProvider.php b/app/Provider/ConsoleProvider.php new file mode 100644 index 0000000..9875713 --- /dev/null +++ b/app/Provider/ConsoleProvider.php @@ -0,0 +1,28 @@ +format('Y-m-d H:i:s'); + + parent::insert($id, $row); + } + + /** + * @param string $id + * @param array $row + */ + protected function update(string $id, array $row) + { + $row['updatedAt'] = (new \DateTime())->format('Y-m-d H:i:s'); + + parent::update($id, $row); + } +} diff --git a/app/Repository/SampleRepository.php b/app/Repository/SampleRepository.php new file mode 100644 index 0000000..a00abd3 --- /dev/null +++ b/app/Repository/SampleRepository.php @@ -0,0 +1,110 @@ +searchCount($sampleSearch); + + $sampleSearch->setPages((int) ceil($count / $sampleSearch->getPerPage())); + $sampleSearch->setCount($count); + $sampleSearch->setSamples($this->searchResult($sampleSearch)); + + return $sampleSearch; + } + + /** + * @param SampleSearch $sampleSearch + * + * @return int + */ + private function searchCount(SampleSearch $sampleSearch): int + { + $qb = $this->prepareSearchQuery($sampleSearch); + $qb->select('COUNT(id) AS rowCount'); + + return (int) $qb->execute()->fetchColumn(); + } + + /** + * @param SampleSearch $sampleSearch + * + * @return array + */ + private function searchResult(SampleSearch $sampleSearch): array + { + $qb = $this->prepareSearchQuery($sampleSearch); + + $perPage = $sampleSearch->getPerPage(); + + $qb->select('*'); + $qb->setFirstResult($sampleSearch->getPage() * $perPage - $perPage); + $qb->setMaxResults($perPage); + + if (null !== $sort = $sampleSearch->getSort()) { + $qb->orderBy($sort, $sampleSearch->getOrder()); + } + + $samples = []; + foreach ($qb->execute()->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $samples[] = $this->fromPersistence($row); + } + + return $samples; + } + + /** + * @param SampleSearch $sampleSearch + * + * @return QueryBuilder + */ + private function prepareSearchQuery(SampleSearch $sampleSearch): QueryBuilder + { + $qb = $this->connection->createQueryBuilder(); + $qb->from($this->getTable()); + + return $qb; + } +} diff --git a/app/Search/Index.php b/app/Search/Index.php new file mode 100644 index 0000000..352af50 --- /dev/null +++ b/app/Search/Index.php @@ -0,0 +1,9 @@ +page; + } + + /** + * @param int $page + * + * @return self + */ + public function setPage(int $page): self + { + $this->page = $page; + + return $this; + } + + /** + * @return int + */ + public function getPerPage() + { + return $this->perPage; + } + + /** + * @param int $perPage + * + * @return self + */ + public function setPerPage(int $perPage): self + { + $this->perPage = $perPage; + + return $this; + } + + /** + * @return null|string + */ + public function getSort() + { + return $this->sort; + } + + /** + * @param null|string $sort + * + * @return self + */ + public function setSort(string $sort = null): self + { + $this->sort = $sort; + + return $this; + } + + /** + * @return null|string + */ + public function getOrder() + { + return $this->order; + } + + /** + * @param null|string $order + * + * @return self + */ + public function setOrder(string $order = null): self + { + $this->order = $order; + + return $this; + } + + /** + * @return int + */ + public function getPages(): int + { + return $this->pages; + } + + /** + * @param int $pages + */ + public function setPages(int $pages) + { + $this->pages = $pages; + } + + /** + * @return int + */ + public function getCount(): int + { + return $this->count; + } + + /** + * @param int $count + */ + public function setCount(int $count) + { + $this->count = $count; + } + + /** + * @return Sample[] + */ + public function getSamples(): array + { + return $this->samples; + } + + /** + * @param Sample[] $samples + */ + public function setSamples(array $samples) + { + $this->samples = $samples; + } +} diff --git a/app/Serialization/ErrorMapping.php b/app/Serialization/ErrorMapping.php new file mode 100644 index 0000000..8cb27bc --- /dev/null +++ b/app/Serialization/ErrorMapping.php @@ -0,0 +1,60 @@ +linkGenerator = $linkGenerator; + } + + /** + * @return string + */ + public function getClass(): string + { + return Index::class; + } + + /** + * @return string + */ + public function getType(): string + { + return 'index'; + } + + /** + * @return FieldMappingInterface[] + */ + public function getFieldMappings(): array + { + return []; + } + + /** + * @return FieldMappingInterface[] + */ + public function getEmbeddedFieldMappings(): array + { + return []; + } + + /** + * @return LinkMappingInterface[] + */ + public function getLinkMappings(): array + { + return [ + new LinkMapping('self', new CallbackLinkSerializer(function () { + return $this->linkGenerator->generateLink('index'); + })), + new LinkMapping('samples', new CallbackLinkSerializer(function () { + return $this->linkGenerator->generateLink('sample_search'); + })), + ]; + } +} diff --git a/app/Serialization/LinkGenerator.php b/app/Serialization/LinkGenerator.php new file mode 100644 index 0000000..91a3932 --- /dev/null +++ b/app/Serialization/LinkGenerator.php @@ -0,0 +1,44 @@ +router = $router; + } + + /** + * @param string $routeName + * @param array $data + * @param array $queryParams + * + * @return LinkInterface + */ + public function generateLink(string $routeName, array $data = [], array $queryParams = []): LinkInterface + { + /** @var Route $route */ + $route = $this->router->getNamedRoute($routeName); + + return new Link( + $this->router->pathFor($routeName, $data, $queryParams), + implode('|', $route->getMethods()) + ); + } +} diff --git a/app/Serialization/SampleMapping.php b/app/Serialization/SampleMapping.php new file mode 100644 index 0000000..43c686f --- /dev/null +++ b/app/Serialization/SampleMapping.php @@ -0,0 +1,88 @@ +linkGenerator = $linkGenerator; + } + + /** + * @return string + */ + public function getClass(): string + { + return Sample::class; + } + + /** + * @return string + */ + public function getType(): string + { + return 'sample'; + } + + /** + * @return FieldMappingInterface[] + */ + public function getFieldMappings(): array + { + return [ + new FieldMapping('id'), + ]; + } + + /** + * @return FieldMappingInterface[] + */ + public function getEmbeddedFieldMappings(): array + { + return []; + } + + /** + * @return LinkMappingInterface[] + */ + public function getLinkMappings(): array + { + return [ + new LinkMapping('read', new CallbackLinkSerializer( + function (Request $request, Sample $sample) { + return $this->linkGenerator->generateLink('sample_read', ['id' => $sample->getId()]); + } + )), + new LinkMapping('update', new CallbackLinkSerializer( + function (Request $request, Sample $sample) { + return $this->linkGenerator->generateLink('sample_update', ['id' => $sample->getId()]); + } + )), + new LinkMapping('delete', new CallbackLinkSerializer( + function (Request $request, Sample $sample) { + return $this->linkGenerator->generateLink('sample_delete', ['id' => $sample->getId()]); + } + )), + ]; + } +} diff --git a/app/Serialization/SampleSearchMapping.php b/app/Serialization/SampleSearchMapping.php new file mode 100644 index 0000000..2836b2c --- /dev/null +++ b/app/Serialization/SampleSearchMapping.php @@ -0,0 +1,122 @@ +linkGenerator = $linkGenerator; + } + + /** + * @return string + */ + public function getClass(): string + { + return SampleSearch::class; + } + + /** + * @return string + */ + public function getType(): string + { + return 'sample-search'; + } + + /** + * @return FieldMappingInterface[] + */ + public function getFieldMappings(): array + { + return [ + new FieldMapping('page', new ValueFieldSerializer( + new PropertyAccessor('page'), + ValueFieldSerializer::CAST_INT) + ), + new FieldMapping('perPage', new ValueFieldSerializer( + new PropertyAccessor('perPage'), + ValueFieldSerializer::CAST_INT) + ), + new FieldMapping('sort'), + new FieldMapping('order'), + ]; + } + + /** + * @return FieldMappingInterface[] + */ + public function getEmbeddedFieldMappings(): array + { + return [ + new FieldMapping('count'), + new FieldMapping('pages'), + new FieldMapping('samples', new CollectionFieldSerializer(new PropertyAccessor('samples'))), + ]; + } + + /** + * @return LinkMappingInterface[] + */ + public function getLinkMappings(): array + { + return [ + new LinkMapping('self', new CallbackLinkSerializer( + function (Request $request, SampleSearch $sampleSearch, array $fields) { + return $this->linkGenerator->generateLink('sample_search', [], $fields); + } + )), + new LinkMapping('prev', new CallbackLinkSerializer( + function (Request $request, SampleSearch $sampleSearch, array $fields) { + if ($sampleSearch->getPage() > 1) { + $fields['page'] -= 1; + + return $this->linkGenerator->generateLink('sample_search', [], $fields); + } + + return new NullLink(); + } + )), + new LinkMapping('next', new CallbackLinkSerializer( + function (Request $request, SampleSearch $sampleSearch, array $fields) { + if ($fields['page'] < $sampleSearch->getPages()) { + $fields['page'] += 1; + + return $this->linkGenerator->generateLink('sample_search', [], $fields); + } + + return new NullLink(); + } + )), + new LinkMapping('create', new CallbackLinkSerializer( + function (Request $request, SampleSearch $sampleSearch, array $fields) { + return $this->linkGenerator->generateLink('sample_create'); + } + )), + ]; + } +} diff --git a/app/Validation/SampleMapping.php b/app/Validation/SampleMapping.php new file mode 100644 index 0000000..b548b8a --- /dev/null +++ b/app/Validation/SampleMapping.php @@ -0,0 +1,51 @@ +resolver = $resolver; + } + + /** + * @return string + */ + public function getClass(): string + { + return Sample::class; + } + + /** + * @return ConstraintInterface[] + */ + public function getConstraints(): array + { + return []; + } + + /** + * @return PropertyMappingInterface[] + */ + public function getPropertyMappings(): array + { + return []; + } +} diff --git a/app/Validation/SampleSearchMapping.php b/app/Validation/SampleSearchMapping.php new file mode 100644 index 0000000..1a8fd45 --- /dev/null +++ b/app/Validation/SampleSearchMapping.php @@ -0,0 +1,37 @@ +extend('settings', function (Collection $settings) use ($config) { + $settings->replace($config['settings']); + + return $settings; +}); + +require_once __DIR__.'/services.php'; + +// project settings +foreach ($config['projectSettings'] as $key => $value) { + $container[$key] = $value; +} + +return $container; diff --git a/app/commands.php b/app/commands.php new file mode 100644 index 0000000..7f5010e --- /dev/null +++ b/app/commands.php @@ -0,0 +1,60 @@ +register(new ConsoleProvider()); + +$container[CreateDatabaseCommand::class] = function () use ($container) { + return new CreateDatabaseCommand($container['db']); +}; + +$container[RunSqlCommand::class] = function () use ($container) { + return new RunSqlCommand($container['db']); +}; + +$container[SchemaUpdateCommand::class] = function () use ($container) { + return new SchemaUpdateCommand($container['db'], __DIR__.'/schema.php'); +}; + +/* @var Container $container */ +$container->extend('console.commands', function (array $commands) use ($container) { + $commands[] = new LazyCommand( + $container, + CreateDatabaseCommand::class, + 'chubbyphp:model:dbal:database:create' + ); + + $commands[] = new LazyCommand( + $container, + RunSqlCommand::class, + 'chubbyphp:model:dbal:database:run:sql', + [ + new InputArgument('sql', InputArgument::REQUIRED, 'The SQL statement to execute.'), + new InputOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of result set.', 7), + ] + ); + + $commands[] = new LazyCommand( + $container, + SchemaUpdateCommand::class, + 'chubbyphp:model:dbal:database:schema:update', + [ + new InputOption('dump', null, InputOption::VALUE_NONE, 'Dumps the generated SQL statements'), + new InputOption('force', 'f', InputOption::VALUE_NONE, 'Executes the generated SQL statements.'), + ] + ); + + return $commands; +}); diff --git a/app/middlewares.php b/app/middlewares.php new file mode 100644 index 0000000..ee7ab43 --- /dev/null +++ b/app/middlewares.php @@ -0,0 +1,11 @@ +group('/api', function () use ($app, $container) { + $app->get('', IndexController::class)->setName('index'); + $app->group('/samples', function () use ($app, $container) { + $app->get('', SampleSearchController::class)->setName('sample_search'); + $app->post('', SampleCreateController::class)->setName('sample_create'); + $app->get('/{id}', SampleReadController::class)->setName('sample_read'); + $app->put('/{id}', SampleUpdateController::class)->setName('sample_update'); + $app->delete('/{id}', SampleDeleteController::class)->setName('sample_delete'); + }); +}); diff --git a/app/schema.php b/app/schema.php new file mode 100644 index 0000000..75889be --- /dev/null +++ b/app/schema.php @@ -0,0 +1,17 @@ +createTable('samples'); +$samples->addColumn('id', 'guid'); +$samples->addColumn('createdAt', 'datetime'); +$samples->addColumn('updatedAt', 'datetime', ['notnull' => false]); +$samples->setPrimaryKey(['id']); + +return $schema; diff --git a/app/services.php b/app/services.php new file mode 100644 index 0000000..209a97c --- /dev/null +++ b/app/services.php @@ -0,0 +1,74 @@ +register(new ApiHttpProvider()); +$container->register(new MonologServiceProvider()); +$container->register(new TranslationProvider()); +$container->register(new SwiftmailerServiceProvider()); + +// extend providers +$container['api-http.request.languages'] = function () use ($container) { + return $container['languages']; +}; + +$container['api-http.response.factory'] = function () { + return new ResponseFactory(); +}; + +$container->extend('translator.providers', function (array $providers) use ($container) { + $providers[] = new LocaleTranslationProvider( + 'de', + array_replace( + require $container['vendorDir'].'/chubbyphp/chubbyphp-validation/translations/de.php', + require $container['vendorDir'].'/chubbyphp/chubbyphp-validation-model/translations/de.php' + ) + ); + $providers[] = new LocaleTranslationProvider( + 'en', + array_replace( + require $container['vendorDir'].'/chubbyphp/chubbyphp-validation/translations/en.php', + require $container['vendorDir'].'/chubbyphp/chubbyphp-validation-model/translations/en.php' + ) + ); + + return $providers; +}); + +// controller +require_once __DIR__.'/services/controller.php'; + +// deserialization +require_once __DIR__.'/services/deserialization.php'; + +// error +$container[ErrorManager::class] = function () use ($container) { + return new ErrorManager($container['translator']); +}; + +// http +$container[ContentNegotiator::class] = function () { + return new ContentNegotiator(); +}; + +// repository +require_once __DIR__.'/services/repository.php'; + +// serializer +require_once __DIR__.'/services/serialization.php'; + +// validation +require_once __DIR__.'/services/validation.php'; diff --git a/app/services/controller.php b/app/services/controller.php new file mode 100644 index 0000000..f1401c8 --- /dev/null +++ b/app/services/controller.php @@ -0,0 +1,70 @@ +register(new DeserializationProvider()); + +$container->extend('deserializer.objectmappings', function (array $objectMappings) use ($container) { + $objectMappings[] = new LazyObjectMapping( + $container, + SampleMapping::class, + Sample::class + ); + + $objectMappings[] = new LazyObjectMapping( + $container, + SampleSearchMapping::class, + SampleSearch::class + ); + + return $objectMappings; +}); + +$container[SampleMapping::class] = function () { + return new SampleMapping(); +}; + +$container[SampleSearchMapping::class] = function () { + return new SampleSearchMapping(); +}; diff --git a/app/services/repository.php b/app/services/repository.php new file mode 100644 index 0000000..9e629fc --- /dev/null +++ b/app/services/repository.php @@ -0,0 +1,34 @@ +register(new DoctrineServiceProvider()); + +$container[ArrayStorageCache::class] = function () { + return new ArrayStorageCache(); +}; + +$container[SampleRepository::class] = function () use ($container) { + return new SampleRepository( + $container['db'], + $container[Resolver::class], + $container[ArrayStorageCache::class], + $container['logger'] + ); +}; + +$container[Resolver::class] = function () use ($container) { + return new Resolver($container, [ + SampleRepository::class, + ]); +}; diff --git a/app/services/serialization.php b/app/services/serialization.php new file mode 100644 index 0000000..59f18e2 --- /dev/null +++ b/app/services/serialization.php @@ -0,0 +1,70 @@ +register(new SerializationProvider()); + +$container->extend('serializer.objectmappings', function (array $objectMappings) use ($container) { + $objectMappings[] = new LazyObjectMapping( + $container, + ErrorMapping::class, + Error::class + ); + + $objectMappings[] = new LazyObjectMapping( + $container, + IndexMapping::class, + Index::class + ); + + $objectMappings[] = new LazyObjectMapping( + $container, + SampleMapping::class, + Sample::class + ); + + $objectMappings[] = new LazyObjectMapping( + $container, + SampleSearchMapping::class, + SampleSearch::class + ); + + return $objectMappings; +}); + +$container[LinkGenerator::class] = function () use ($container) { + return new LinkGenerator($container['router']); +}; + +$container[ErrorMapping::class] = function () { + return new ErrorMapping(); +}; + +$container[IndexMapping::class] = function () use ($container) { + return new IndexMapping($container[LinkGenerator::class]); +}; + +$container[SampleMapping::class] = function () use ($container) { + return new SampleMapping($container[LinkGenerator::class]); +}; + +$container[SampleSearchMapping::class] = function () use ($container) { + return new SampleSearchMapping($container[LinkGenerator::class]); +}; diff --git a/app/services/validation.php b/app/services/validation.php new file mode 100644 index 0000000..8eda6bf --- /dev/null +++ b/app/services/validation.php @@ -0,0 +1,42 @@ +register(new ValidationProvider()); + +$container->extend('validator.objectmappings', function (array $objectMappings) use ($container) { + $objectMappings[] = new LazyObjectMapping( + $container, + SampleMapping::class, + Sample::class + ); + + $objectMappings[] = new LazyObjectMapping( + $container, + SampleSearchMapping::class, + SampleSearch::class + ); + + return $objectMappings; +}); + +$container[SampleMapping::class] = function () use ($container) { + return new SampleMapping($container[Resolver::class]); +}; + +$container[SampleSearchMapping::class] = function () { + return new SampleSearchMapping(); +}; diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..6b5d5ca --- /dev/null +++ b/bin/console @@ -0,0 +1,37 @@ +#!/usr/bin/env php +getParameterOption(['--env', '-e'], 'dev'); + +/** @var Container $container */ +$container = require_once __DIR__ . '/../app/bootstrap.php'; + +require_once __DIR__ . '/../app/commands.php'; + +$console = new Application($container['console.name'], $container['console.version']); + +$inputOptions = [ + new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', 'dev') +]; + +foreach ($inputOptions as $inputOption) { + $console->getDefinition()->addOption($inputOption); +} + +foreach ($container['console.helpers'] as $alias => $helper) { + $console->getHelperSet()->set($helper, $alias); +} + +$console->addCommands($container['console.commands']); +$console->run($input); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..518e426 --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "chubbyphp/chubbyphp-api-slim-skeleton", + "type": "project", + "require": { + "php": "~7.0", + "chubbyphp/chubbyphp-api-http": "~1.0@beta", + "chubbyphp/chubbyphp-deserialization": "~1.1", + "chubbyphp/chubbyphp-deserialization-model": "~1.0", + "chubbyphp/chubbyphp-lazy": "~1.1", + "chubbyphp/chubbyphp-model": "~3.0", + "chubbyphp/chubbyphp-model-doctrine-dbal": "~1.0", + "chubbyphp/chubbyphp-serialization": "^1.0.1", + "chubbyphp/chubbyphp-translation": "^1.1.2", + "chubbyphp/chubbyphp-validation": "~2.1", + "chubbyphp/chubbyphp-validation-model": "~1.0", + "monolog/monolog": "~1.21", + "ramsey/uuid": "~3.5", + "silex/providers": "^2.0.3", + "slim/slim": "~3.5", + "symfony/console": "~3.1", + "symfony/yaml": "~3.1", + "willdurand/negotiation": "^2.3" + }, + "require-dev": { + "phpunit/phpunit": "~6.3" + }, + "autoload": { + "psr-4": { "Chubbyphp\\ApiSkeleton\\": "app/" } + } +} diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..5556f92 --- /dev/null +++ b/config/config.php @@ -0,0 +1,40 @@ + [ + 'httpVersion' => '1.1', + 'responseChunkSize' => 4096, + 'outputBuffering' => 'append', + 'determineRouteBeforeAppMiddleware' => true, + 'displayErrorDetails' => false, + 'addContentLengthHeader' => true, + 'routerCacheFile' => $container['cacheDir'].'/routes.php', + ], + 'projectSettings' => [ + 'db.options' => [ + 'driver' => 'pdo_mysql', + 'host' => 'localhost', + 'port' => 3306, + 'dbname' => 'chubbyphp_api_skeleton', + 'user' => 'root', + 'password' => 'root', + 'charset' => 'utf8', + ], + 'debug' => false, + 'defaultLanguage' => 'en', + 'languages' => ['de', 'en'], + 'monolog.logfile' => $container['logDir'].'/application-'.(new \DateTime())->format('Y-m-d').'.log', + 'monolog.level' => 'notice', + 'swiftmailer.options' => [ + 'host' => 'smtp.gmail.com', + 'port' => '465', + 'username' => 'firstname.lastname@gmail.com', + 'password' => 'MhZtGShTa65Gta54', + 'encryption' => 'ssl', + 'auth_mode' => 'login' + ], + 'swiftmailer.use_spool' => false + ], +]; diff --git a/config/config_dev.php b/config/config_dev.php new file mode 100644 index 0000000..82692ca --- /dev/null +++ b/config/config_dev.php @@ -0,0 +1,20 @@ + [ + 'displayErrorDetails' => true, + 'routerCacheFile' => false, + ], + 'projectSettings' => [ + 'debug' => true, + 'monolog.level' => 'debug', + 'swiftmailer.options' => [ + 'host' => 'localhost', + 'port' => '25', + 'username' => null, + 'password' => null, + 'encryption' => null, + 'auth_mode' => null + ], + ], +]; diff --git a/config/config_prod.php b/config/config_prod.php new file mode 100644 index 0000000..0b67a5f --- /dev/null +++ b/config/config_prod.php @@ -0,0 +1,3 @@ + [ + 'displayErrorDetails' => true, + 'routerCacheFile' => false, + ], + 'projectSettings' => [ + 'db.options' => [ + 'dbname' => 'chubbyphp_api_skeleton_test', + ], + ], +]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..bb6f71a --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,32 @@ + + + + + ./tests/Integration + + + ./tests/Unit + + + + + ./app + + + + + + + + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..65b0e80 --- /dev/null +++ b/public/index.php @@ -0,0 +1,12 @@ +run(); diff --git a/public/index_dev.php b/public/index_dev.php new file mode 100644 index 0000000..83c8607 --- /dev/null +++ b/public/index_dev.php @@ -0,0 +1,47 @@ += 16 && $matches[2] <= 31) { + return true; + } + if($matches[1] === 192 && $matches[2] === 168) { + return true; + } + } +} + +if (isset($_SERVER['HTTP_CLIENT_IP']) + || isset($_SERVER['HTTP_X_FORWARDED_FOR']) + || !(checkAllowedIp($_SERVER['REMOTE_ADDR']) || php_sapi_name() === 'cli-server') +) { + header('HTTP/1.0 403 Forbidden'); + exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.'); +} + +$loader = require_once __DIR__.'/../app/autoload.php'; + +$env = 'dev'; + +/** @var \Slim\App $app */ +$app = require_once __DIR__ . '/../app/app.php'; + +$app->run(); diff --git a/public/index_test.php b/public/index_test.php new file mode 100644 index 0000000..6bb4f39 --- /dev/null +++ b/public/index_test.php @@ -0,0 +1,17 @@ +run(); diff --git a/tests/Integration/AbstractIntegrationTest.php b/tests/Integration/AbstractIntegrationTest.php new file mode 100644 index 0000000..a54a13b --- /dev/null +++ b/tests/Integration/AbstractIntegrationTest.php @@ -0,0 +1,128 @@ + $value) { + $curlHeaders[] = sprintf('%s: %s', $key, implode(', ', (array) $value)); + } + + if (null === $this->curl) { + $this->curl = $this->initializeCurl(); + } + + curl_setopt($this->curl, CURLOPT_URL, sprintf('http://localhost:%d%s', PhpServerListener::PHP_SERVER_PORT, $resource)); + curl_setopt($this->curl, CURLOPT_HTTPHEADER, $curlHeaders); + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($this->curl, CURLOPT_POSTFIELDS, $body); + + $rawResponse = curl_exec($this->curl); + if (false === $rawResponse) { + throw new \RuntimeException('Invalid response from server!'); + } + + $headerSize = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); + + $headerRows = $this->getHttpHeaderRows($rawResponse, $headerSize); + + $status = $this->getHttpStatus(array_shift($headerRows)); + $headers = $this->geHttptHeaders($headerRows); + + $body = substr($rawResponse, $headerSize); + if ('' === $body) { + $body = null; + } + + return ['status' => $status, 'headers' => $headers, 'body' => $body]; + } + + /** + * @return resource + */ + private function initializeCurl() + { + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + + return $ch; + } + + /** + * @param string $rawResponse + * @param int $headerSize + * + * @return array + */ + private function getHttpHeaderRows(string $rawResponse, int $headerSize): array + { + $headerRawGroups = explode("\r\n\r\n", trim(substr($rawResponse, 0, $headerSize))); + + return explode("\r\n", $headerRawGroups[0]); + } + + /** + * @param string $statusRow + * + * @return array + */ + private function getHttpStatus(string $statusRow): array + { + $matches = []; + preg_match('#^HTTP/1.\d{1} (\d+) (.+)$#', $statusRow, $matches); + + return [ + 'code' => (int) $matches[1], + 'message' => $matches[2], + ]; + } + + /** + * @param array $headerRows + * + * @return array + */ + private function geHttptHeaders(array $headerRows): array + { + $headers = []; + foreach ($headerRows as $headerRow) { + if (false !== $pos = strpos($headerRow, ':')) { + $key = strtolower(trim(substr($headerRow, 0, $pos))); + $value = strtolower(trim(substr($headerRow, $pos + 1))); + if ('' !== $value) { + if (!isset($headers[$key])) { + $headers[$key] = []; + } + $headers[$key][] = $value; + } + } + } + + ksort($headers); + + return $headers; + } +} diff --git a/tests/Integration/Controller/IndexControllerTest.php b/tests/Integration/Controller/IndexControllerTest.php new file mode 100644 index 0000000..ad5d4f0 --- /dev/null +++ b/tests/Integration/Controller/IndexControllerTest.php @@ -0,0 +1,35 @@ +httpRequest('GET', '/api', ['Accept' => 'application/json']); + + self::assertSame(200, $response['status']['code']); + + self::assertArrayHasKey('content-type', $response['headers']); + + self::assertSame('application/json', $response['headers']['content-type'][0]); + + $data = json_decode($response['body'], true); + + self::assertEquals([ + '_links' => [ + 'self' => [ + 'href' => '/api', + 'method' => 'GET', + ], + 'samples' => [ + 'href' => '/api/samples', + 'method' => 'GET', + ], + ], + '_type' => 'index', + ], $data); + } +} diff --git a/tests/Integration/Controller/SampleControllerTest.php b/tests/Integration/Controller/SampleControllerTest.php new file mode 100644 index 0000000..b9ea5b1 --- /dev/null +++ b/tests/Integration/Controller/SampleControllerTest.php @@ -0,0 +1,148 @@ +httpRequest('GET', '/api/samples', ['Accept' => 'application/json']); + + self::assertSame(200, $response['status']['code']); + + self::assertArrayHasKey('content-type', $response['headers']); + + self::assertSame('application/json', $response['headers']['content-type'][0]); + + $data = json_decode($response['body'], true); + + self::assertSame(1, $data['page']); + self::assertSame(20, $data['perPage']); + self::assertNull($data['sort']); + self::assertSame('asc', $data['order']); + + self::assertArrayHasKey('_embedded', $data); + self::assertArrayHasKey('count', $data['_embedded']); + self::assertArrayHasKey('pages', $data['_embedded']); + self::assertArrayHasKey('samples', $data['_embedded']); + } + + /** + * @return string + */ + public function testCreate() + { + $response = $this->httpRequest('POST', '/api/samples', + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + json_encode([]) + ); + + self::assertSame(201, $response['status']['code']); + + self::assertArrayHasKey('content-type', $response['headers']); + + self::assertSame('application/json', $response['headers']['content-type'][0]); + + $data = json_decode($response['body'], true); + + self::assertArrayHasKey('id', $data); + + self::assertArrayHasKey('_links', $data); + + self::assertArrayHasKey('read', $data['_links']); + self::assertSame('GET', $data['_links']['read']['method']); + + self::assertArrayHasKey('update', $data['_links']); + self::assertSame('PUT', $data['_links']['update']['method']); + + self::assertArrayHasKey('delete', $data['_links']); + self::assertSame('DELETE', $data['_links']['delete']['method']); + + return $data['id']; + } + + /** + * @depends testCreate + */ + public function testRead(string $id) + { + $response = $this->httpRequest('GET', '/api/samples/'.$id, + [ + 'Accept' => 'application/json', + ] + ); + + self::assertSame(200, $response['status']['code']); + + self::assertArrayHasKey('content-type', $response['headers']); + + self::assertSame('application/json', $response['headers']['content-type'][0]); + + $data = json_decode($response['body'], true); + + self::assertArrayHasKey('id', $data); + + self::assertArrayHasKey('_links', $data); + + self::assertArrayHasKey('read', $data['_links']); + self::assertSame('GET', $data['_links']['read']['method']); + + self::assertArrayHasKey('update', $data['_links']); + self::assertSame('PUT', $data['_links']['update']['method']); + + self::assertArrayHasKey('delete', $data['_links']); + self::assertSame('DELETE', $data['_links']['delete']['method']); + } + + /** + * @depends testCreate + */ + public function testUpdate(string $id) + { + $response = $this->httpRequest('PUT', '/api/samples/'.$id, + [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + json_encode([]) + ); + + self::assertSame(200, $response['status']['code']); + + self::assertArrayHasKey('content-type', $response['headers']); + + self::assertSame('application/json', $response['headers']['content-type'][0]); + + $data = json_decode($response['body'], true); + + self::assertArrayHasKey('id', $data); + + self::assertArrayHasKey('_links', $data); + + self::assertArrayHasKey('read', $data['_links']); + self::assertSame('GET', $data['_links']['read']['method']); + + self::assertArrayHasKey('update', $data['_links']); + self::assertSame('PUT', $data['_links']['update']['method']); + + self::assertArrayHasKey('delete', $data['_links']); + self::assertSame('DELETE', $data['_links']['delete']['method']); + } + + /** + * @depends testCreate + */ + public function testDelete(string $id) + { + $response = $this->httpRequest('DELETE', '/api/samples/'.$id); + + self::assertSame(204, $response['status']['code']); + + self::assertNull($response['body']); + } +} diff --git a/tests/PhpServerListener.php b/tests/PhpServerListener.php new file mode 100644 index 0000000..ac97c74 --- /dev/null +++ b/tests/PhpServerListener.php @@ -0,0 +1,89 @@ +getName(), 'Integration')) { + return; + } + + $this->killPhpServer(); + + exec(sprintf( + '%s > %s 2>&1 & echo $! >> %s', + $this->getCommand(), + $this->getLogFile(), + $this->getPidFile() + )); + + usleep(100000); + } + + /** + * @param TestSuite $suite + */ + public function endTestSuite(TestSuite $suite) + { + if (false === strpos($suite->getName(), 'Integration')) { + return; + } + + $this->killPhpServer(); + } + + /** + * @return string + */ + private function getCommand(): string + { + return sprintf( + 'php -S localhost:%d -t %s %s', + self::PHP_SERVER_PORT, + __DIR__.'/../public', + __DIR__.'/../public/index_test.php' + ); + } + + private function killPhpServer() + { + if (!is_file(self::PHP_SERVER_PIDFILE)) { + return; + } + + $pid = (int) file_get_contents($this->getPidFile()); + + exec(sprintf('/bin/kill %d', $pid)); + + unlink($this->getPidFile()); + unlink($this->getLogFile()); + } + + /** + * @return string + */ + private function getPidFile(): string + { + return sys_get_temp_dir().'/'.self::PHP_SERVER_PIDFILE; + } + + /** + * @return string + */ + private function getLogFile(): string + { + return sys_get_temp_dir().'/'.self::PHP_SERVER_LOGFILE; + } +} diff --git a/tests/Unit/.gitkeep b/tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..d312962 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,53 @@ +setPsr4('Chubbyphp\Tests\ApiSkeleton\\', __DIR__); + +$env = 'test'; + +/** @var \Slim\Container $container */ +$container = require_once __DIR__.'/../app/bootstrap.php'; + +$removeCacheDirCommand = 'rm -Rf '.$container['cacheDir']; +$removeLogDirCommand = 'rm -Rf '.$container['logDir']; + +echo execute($removeCacheDirCommand); +echo execute($removeLogDirCommand); + +$dropDatabaseCommand = sprintf( + 'mysql -h%s -P%d -u%s -p%s -e \'DROP DATABASE `%s`;\'', + $container['db.options']['host'], + $container['db.options']['port'], + $container['db.options']['user'], + $container['db.options']['password'], + $container['db.options']['dbname'] +); + +echo execute($dropDatabaseCommand); + +$commands = [ + 'bin/console chubbyphp:model:dbal:database:create --env=test', + 'bin/console chubbyphp:model:dbal:database:schema:update --dump --force --env=test', +]; + +foreach ($commands as $command) { + echo execute('cd '.$container['rootDir'].' && '.$command); +} + +echo PHP_EOL; + +/** + * @param string $command + * + * @return string + */ +function execute(string $command): string +{ + $output = [ + 'command: '.$command, + ]; + + exec($command, $output); + + return implode(PHP_EOL, $output).PHP_EOL.PHP_EOL; +} diff --git a/vagrant-php b/vagrant-php new file mode 160000 index 0000000..e5e0b3e --- /dev/null +++ b/vagrant-php @@ -0,0 +1 @@ +Subproject commit e5e0b3eeccd66bac3d065c0d1f79115d0a32d8f4 diff --git a/vagrant.yml b/vagrant.yml new file mode 100644 index 0000000..0472f54 --- /dev/null +++ b/vagrant.yml @@ -0,0 +1,4 @@ +hostname: chubbyphp-api-slim-skeleton.dev +application: slim3 +network: + ip: 10.132.131.130