├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .env.example ├── .eslintrc ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .php_cs.dist ├── LICENSE ├── app ├── Console │ └── Kernel.php ├── Eloquents │ ├── AccessToken.php │ ├── Account.php │ ├── Category.php │ ├── CategoryName.php │ ├── CategoryStock.php │ ├── LoginSession.php │ ├── QiitaAccount.php │ └── QiitaUserName.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ ├── AccountController.php │ │ ├── Auth │ │ │ ├── ForgotPasswordController.php │ │ │ ├── LoginController.php │ │ │ ├── RegisterController.php │ │ │ └── ResetPasswordController.php │ │ ├── CategoryController.php │ │ ├── Controller.php │ │ ├── LoginSessionController.php │ │ ├── StatusController.php │ │ └── StockController.php │ ├── Kernel.php │ └── Middleware │ │ ├── CheckForMaintenanceMode.php │ │ ├── CheckMaintenance.php │ │ ├── Cors.php │ │ ├── EncryptCookies.php │ │ ├── InfoLogging.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustProxies.php │ │ ├── VerifyCsrfToken.php │ │ └── XRequestId.php ├── Infrastructure │ ├── Logger.php │ └── Repositories │ │ ├── Api │ │ ├── QiitaApiRepository.php │ │ └── Repository.php │ │ └── Eloquent │ │ ├── AccountRepository.php │ │ ├── CategoryRepository.php │ │ └── LoginSessionRepository.php ├── Models │ └── Domain │ │ ├── Account │ │ ├── AccountEntity.php │ │ ├── AccountEntityBuilder.php │ │ ├── AccountRepository.php │ │ └── AccountSpecification.php │ │ ├── Category │ │ ├── CategoryEntities.php │ │ ├── CategoryEntity.php │ │ ├── CategoryEntityBuilder.php │ │ ├── CategoryNameValue.php │ │ ├── CategoryRepository.php │ │ ├── CategorySpecification.php │ │ ├── CategoryStockEntities.php │ │ ├── CategoryStockEntity.php │ │ └── CategoryStockEntityBuilder.php │ │ ├── ErrorResponseEntity.php │ │ ├── ErrorResponseEntityBuilder.php │ │ ├── Exceptions │ │ ├── AccountCreatedException.php │ │ ├── AccountNotFoundException.php │ │ ├── BusinessLogicException.php │ │ ├── CategoryNotFoundException.php │ │ ├── CategoryRelationNotFoundException.php │ │ ├── LoginSessionExpiredException.php │ │ ├── MaintenanceException.php │ │ ├── ServiceUnavailableException.php │ │ ├── UnauthorizedException.php │ │ └── ValidationException.php │ │ ├── LoginSession │ │ ├── LoginSessionEntity.php │ │ ├── LoginSessionEntityBuilder.php │ │ ├── LoginSessionRepository.php │ │ └── LoginSessionSpecification.php │ │ ├── QiitaAccountValue.php │ │ ├── QiitaAccountValueBuilder.php │ │ ├── QiitaApiRepository.php │ │ └── Stock │ │ ├── FetchStockValues.php │ │ ├── LinkHeaderService.php │ │ ├── LinkHeaderValue.php │ │ ├── StockSpecification.php │ │ ├── StockValue.php │ │ ├── StockValueBuilder.php │ │ └── StockValues.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── Services │ ├── AccountScenario.php │ ├── Authentication.php │ ├── CategoryScenario.php │ ├── LoginSessionScenario.php │ └── StockScenario.php └── User.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── buildspec-migration.yml ├── buildspec-push-ecr.yml ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── database.php ├── filesystems.php ├── hashing.php ├── logging.php ├── mail.php ├── queue.php ├── services.php ├── session.php └── view.php ├── createDotenv.js ├── database ├── .gitignore ├── database.sql ├── factories │ ├── AccountFactory.php │ ├── CategoryFactory.php │ └── UserFactory.php ├── migrations │ ├── 2018_10_01_174932_create_accounts_table.php │ ├── 2018_10_02_070100_create_accounts_qiita_accounts_table.php │ ├── 2018_10_02_070854_create_accounts_access_tokens_table.php │ ├── 2018_10_02_072202_create_login_sessions_table.php │ ├── 2018_11_13_002016_create_categories_table.php │ ├── 2018_11_13_005301_create_categories_names_table.php │ ├── 2018_12_10_004032_create_accounts_qiita_user_names_table.php │ └── 2018_12_25_114434_create_categories_stocks_table.php └── seeds │ └── DatabaseSeeder.php ├── deployUtils.js ├── docker-compose.yml ├── docker ├── nginx │ ├── Dockerfile │ └── config │ │ ├── default.conf.template │ │ └── nginx.conf └── php │ ├── Dockerfile │ └── config │ ├── docker-php-ext-opcache.ini │ └── php.ini ├── package.json ├── phpunit.xml ├── public ├── .htaccess ├── css │ └── app.css ├── favicon.ico ├── index.php ├── js │ └── app.js ├── robots.txt └── web.config ├── push-ecr-local.sh ├── readme.md ├── resources ├── assets │ ├── js │ │ ├── app.js │ │ ├── bootstrap.js │ │ └── components │ │ │ └── ExampleComponent.vue │ └── sass │ │ ├── _variables.scss │ │ └── app.scss ├── lang │ └── en │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ └── validation.php └── views │ └── welcome.blade.php ├── routes ├── api.php ├── channels.php ├── console.php └── web.php ├── server.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── CreatesApplication.php ├── Feature │ ├── AbstractTestCase.php │ ├── AccountCreateTest.php │ ├── AccountDestroyTest.php │ ├── CategoryCategorizeTest.php │ ├── CategoryCreateTest.php │ ├── CategoryDestroyRelationTest.php │ ├── CategoryDestroyTest.php │ ├── CategoryIndexTest.php │ ├── CategoryUpdateTest.php │ ├── ExampleTest.php │ ├── LoginSessionCreateTest.php │ ├── LoginSessionDestroyTest.php │ ├── StatusIndexTest.php │ ├── StockIndexTest.php │ └── StockShowCategorizedTest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/php:7.3.4-fpm-stretch 6 | - image: circleci/mysql:5.7 7 | environment: 8 | - APP_NAME: qiita-stocker-backend 9 | - APP_DEBUG: true 10 | - APP_ENV: testing 11 | - APP_KEY: base64:EV4IJnxLNQdTyf7lKiDSxAuXVctckPHzePPfZe0DBBo= 12 | - APP_URL: http://127.0.0.1 13 | - DB_CONNECTION: circle_test 14 | - MYSQL_ALLOW_EMPTY_PASSWORD: true 15 | - CORS_ORIGIN: http://127.0.0.1 16 | - BROADCAST_DRIVER: log 17 | - MAINTENANCE_MODE: false 18 | - LOG_CHANNEL: app 19 | - NOTIFICATION_SLACK_CHANNEL: ${NOTIFICATION_SLACK_CHANNEL} 20 | - NOTIFICATION_SLACK_TOKEN: ${NOTIFICATION_SLACK_TOKEN} 21 | 22 | working_directory: ~/repo 23 | 24 | steps: 25 | - checkout 26 | - run: sudo docker-php-ext-install pdo_mysql 27 | - restore_cache: 28 | keys: 29 | - v1-dependencies-{{ checksum "composer.json" }} 30 | - v1-dependencies- 31 | - run: composer install -n --prefer-dist 32 | - save_cache: 33 | paths: 34 | - ./vendor 35 | key: v1-dependencies-{{ checksum "composer.json" }} 36 | - run: php artisan migrate 37 | - run: php artisan db:seed 38 | - run: php ./vendor/bin/phpunit 39 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.circleci 3 | /.git 4 | /.github 5 | /coverage 6 | /node_modules 7 | /bootstrap/cache/* 8 | /storage/app/* 9 | /storage/framework/cache/* 10 | /storage/framework/sessions/* 11 | /storage/framework/views/* 12 | /storage/logs/* 13 | /vendor 14 | deployUtils.js 15 | createDotenv.js 16 | package.json 17 | yarn.lock 18 | .env* 19 | LICENSE 20 | qiita-stocker-backend.iml 21 | readme.md 22 | push-ecr-local.sh 23 | buildspec-migration.yml 24 | buildspec-push-ecr.yml 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | 17 | [*.js] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | 9 | DB_CONNECTION=mysql 10 | DB_HOST=127.0.0.1 11 | DB_PORT=3306 12 | DB_DATABASE=homestead 13 | DB_USERNAME=homestead 14 | DB_PASSWORD=secret 15 | 16 | BROADCAST_DRIVER=log 17 | CACHE_DRIVER=file 18 | SESSION_DRIVER=file 19 | SESSION_LIFETIME=120 20 | QUEUE_DRIVER=sync 21 | 22 | REDIS_HOST=127.0.0.1 23 | REDIS_PASSWORD=null 24 | REDIS_PORT=6379 25 | 26 | MAIL_DRIVER=smtp 27 | MAIL_HOST=smtp.mailtrap.io 28 | MAIL_PORT=2525 29 | MAIL_USERNAME=null 30 | MAIL_PASSWORD=null 31 | MAIL_ENCRYPTION=null 32 | 33 | PUSHER_APP_ID= 34 | PUSHER_APP_KEY= 35 | PUSHER_APP_SECRET= 36 | PUSHER_APP_CLUSTER=mt1 37 | 38 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 39 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 40 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "rules": { 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "trailingComma": "es5" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # PRを出す前に確認する事 2 | 3 | 以下の条件を満たしているか再度チェックしよう🐱! 4 | 5 | - PRのタイトルは分かりやすい事(理想は非エンジニアが見ても何となく分かるように、無理な場合も多いけど・・・) 6 | - READMEに書いてるセルフチェックが全て完了している事 7 | - e.g. Lintでのコード整形 8 | - e.g テストコードの実装 9 | - PRテンプレートに従って必要な情報が記載されている事 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: バグ報告 3 | about: バグの報告用テンプレート 4 | 5 | --- 6 | 7 | # 概要 8 | [required] 9 | 10 | # 再現手順 11 | [required] 12 | 13 | # 再現環境 14 | [required] 15 | 16 | # このバグによって引き起こされる問題 17 | [optional] 18 | 19 | # スクリーンショット 20 | [optional] 21 | 22 | # 補足情報 23 | [optional] 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ストーリー 3 | about: スクラム開発でいうところのストーリーのテンプレート 4 | 5 | --- 6 | 7 | # Doneの定義 8 | [required] 9 | 10 | # 補足情報 11 | [optional] 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # issueURL 2 | [required] 3 | 4 | # 関連URL 5 | [optional] 6 | 7 | # Doneの定義 8 | [required] 9 | 10 | # スクリーンショット 11 | [optional] ただしUI変更の時は [required] 12 | 13 | # 変更点概要 14 | 15 | ## 仕様的変更点概要 16 | [optional] 17 | 18 | ## 技術的変更点概要 19 | [required] 20 | 21 | # レビュアーに重点的にチェックして欲しい点 22 | [optional] 23 | 24 | # 補足情報 25 | [optional] 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /vendor 6 | /.idea 7 | /.vscode 8 | /.vagrant 9 | /coverage 10 | Homestead.json 11 | Homestead.yaml 12 | npm-debug.log 13 | yarn-error.log 14 | .env 15 | .env.testing 16 | .phpunit.result.cache 17 | qiita-stocker-backend.iml 18 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | exclude('bootstrap/cache') 5 | ->exclude('storage') 6 | ->exclude('vendor') 7 | ->exclude('resources/lang') 8 | ->in(__DIR__) 9 | ->name('*.php') 10 | ->notName('*.blade.php') 11 | ->ignoreDotFiles(true); 12 | 13 | $config = PhpCsFixer\Config::create(); 14 | $rules = [ 15 | '@PSR1' => false, 16 | '@PSR2' => true, 17 | 'array_syntax' => ['syntax' => 'short'], // 配列は[]に統一 18 | 'no_multiline_whitespace_around_double_arrow' => true, // 演算子 => は複数行の空白に囲まれない 19 | 'no_multiline_whitespace_before_semicolons' => true, // セミコロンを閉じる前の複数行の空白は禁止 20 | 'blank_line_after_namespace' => true, // 名前空間の後に空行を追加 21 | 'no_unused_imports' => true, // 未使用のuse文は削除 22 | 'ordered_imports' => ['sortAlgorithm' => 'length'], // use文の整列 23 | 'single_quote' => true, // 単純な文字列の二重引用符を一重引用符に変換 24 | 'binary_operator_spaces' => [ 25 | 'align_double_arrow' => true, 26 | ], // = , => を整列する 27 | 'increment_style' => ['style' => 'post'], // インクリメントおよびデクリメント演算子の統一 28 | 'linebreak_after_opening_tag' => true, // 開始タグの後ろに改行を入れる 29 | 'simplified_null_return' => true, // return nullを簡略化する 30 | ]; 31 | 32 | $config->setRules($rules) 33 | ->setUsingCache(false) 34 | ->setFinder($finder); 35 | 36 | return $config; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 nekochans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire') 28 | // ->hourly(); 29 | } 30 | 31 | /** 32 | * Register the commands for the application. 33 | * 34 | * @return void 35 | */ 36 | protected function commands() 37 | { 38 | $this->load(__DIR__.'/Commands'); 39 | 40 | require base_path('routes/console.php'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Eloquents/AccessToken.php: -------------------------------------------------------------------------------- 1 | accountScenario = $accountScenario; 31 | } 32 | 33 | /** 34 | * アカウントを登録する 35 | * 36 | * @param Request $request 37 | * @return JsonResponse 38 | * @throws \App\Models\Domain\Exceptions\AccountCreatedException 39 | * @throws \App\Models\Domain\Exceptions\ValidationException 40 | */ 41 | public function create(Request $request): JsonResponse 42 | { 43 | $requestArray = $request->json()->all(); 44 | 45 | $sessionId = $this->accountScenario->create($requestArray); 46 | 47 | return response()->json($sessionId)->setStatusCode(201); 48 | } 49 | 50 | /** 51 | * アカウントを削除する 52 | * 53 | * @param Request $request 54 | * @return JsonResponse 55 | * @throws \App\Models\Domain\Exceptions\LoginSessionExpiredException 56 | * @throws \App\Models\Domain\Exceptions\UnauthorizedException 57 | */ 58 | public function destroy(Request $request): JsonResponse 59 | { 60 | $sessionId = $request->bearerToken(); 61 | 62 | $params = [ 63 | 'sessionId' => $sessionId 64 | ]; 65 | 66 | $this->accountScenario->destroy($params); 67 | return response()->json()->setStatusCode(204); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ForgotPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/LoginController.php: -------------------------------------------------------------------------------- 1 | middleware('guest')->except('logout'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisterController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 41 | } 42 | 43 | /** 44 | * Get a validator for an incoming registration request. 45 | * 46 | * @param array $data 47 | * @return \Illuminate\Contracts\Validation\Validator 48 | */ 49 | protected function validator(array $data) 50 | { 51 | return Validator::make($data, [ 52 | 'name' => 'required|string|max:255', 53 | 'email' => 'required|string|email|max:255|unique:users', 54 | 'password' => 'required|string|min:6|confirmed', 55 | ]); 56 | } 57 | 58 | /** 59 | * Create a new user instance after a valid registration. 60 | * 61 | * @param array $data 62 | * @return \App\User 63 | */ 64 | protected function create(array $data) 65 | { 66 | return User::create([ 67 | 'name' => $data['name'], 68 | 'email' => $data['email'], 69 | 'password' => Hash::make($data['password']), 70 | ]); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ResetPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | loginSessionScenario = $loginSessionScenario; 31 | } 32 | 33 | /** 34 | * ログインセッションを発行する 35 | * 36 | * @param Request $request 37 | * @return JsonResponse 38 | * @throws \App\Models\Domain\Exceptions\AccountNotFoundException 39 | * @throws \App\Models\Domain\Exceptions\ValidationException 40 | */ 41 | public function create(Request $request): JsonResponse 42 | { 43 | $requestArray = $request->json()->all(); 44 | 45 | $sessionId = $this->loginSessionScenario->create($requestArray); 46 | 47 | return response()->json($sessionId)->setStatusCode(201); 48 | } 49 | 50 | /** 51 | * ログインセッションを削除する 52 | * 53 | * @param Request $request 54 | * @return JsonResponse 55 | * @throws \App\Models\Domain\Exceptions\LoginSessionExpiredException 56 | * @throws \App\Models\Domain\Exceptions\UnauthorizedException 57 | */ 58 | public function destroy(Request $request): JsonResponse 59 | { 60 | $sessionId = $request->bearerToken(); 61 | $params = [ 62 | 'sessionId' => $sessionId 63 | ]; 64 | 65 | $this->loginSessionScenario->destroy($params); 66 | 67 | return response()->json()->setStatusCode(204); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Controllers/StatusController.php: -------------------------------------------------------------------------------- 1 | json()->setStatusCode(200); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/StockController.php: -------------------------------------------------------------------------------- 1 | stockScenario = $stockScenario; 31 | } 32 | 33 | /** 34 | * ストック一覧を取得する 35 | * 36 | * @param Request $request 37 | * @return JsonResponse 38 | * @throws \App\Models\Domain\Exceptions\LoginSessionExpiredException 39 | * @throws \App\Models\Domain\Exceptions\ServiceUnavailableException 40 | * @throws \App\Models\Domain\Exceptions\UnauthorizedException 41 | * @throws \App\Models\Domain\Exceptions\ValidationException 42 | */ 43 | public function index(Request $request): JsonResponse 44 | { 45 | $sessionId = $request->bearerToken(); 46 | $params = [ 47 | 'sessionId' => $sessionId, 48 | 'page' => $request->query('page'), 49 | 'perPage' => $request->query('per_page'), 50 | 'uri' => env('APP_URL') . $request->getPathInfo() 51 | ]; 52 | 53 | $response = $this->stockScenario->index($params); 54 | 55 | return response() 56 | ->json($response['stocks']) 57 | ->setStatusCode(200) 58 | ->header('Total-Count', $response['totalCount']) 59 | ->header('Link', $response['link']); 60 | } 61 | 62 | /** 63 | * カテゴライズされたストック一覧を取得する 64 | * 65 | * @param Request $request 66 | * @return JsonResponse 67 | * @throws \App\Models\Domain\Exceptions\CategoryNotFoundException 68 | * @throws \App\Models\Domain\Exceptions\LoginSessionExpiredException 69 | * @throws \App\Models\Domain\Exceptions\UnauthorizedException 70 | * @throws \App\Models\Domain\Exceptions\ValidationException 71 | */ 72 | public function showCategorized(Request $request): JsonResponse 73 | { 74 | $sessionId = $request->bearerToken(); 75 | $params = [ 76 | 'sessionId' => $sessionId, 77 | 'id' => $request->id, 78 | 'page' => $request->query('page'), 79 | 'perPage' => $request->query('per_page'), 80 | 'uri' => env('APP_URL') . $request->getPathInfo() 81 | ]; 82 | 83 | $response = $this->stockScenario->showCategorized($params); 84 | 85 | return response() 86 | ->json($response['stocks']) 87 | ->setStatusCode(200) 88 | ->header('Total-Count', $response['totalCount']) 89 | ->header('Link', $response['link']); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 32 | \App\Http\Middleware\EncryptCookies::class, 33 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 34 | \Illuminate\Session\Middleware\StartSession::class, 35 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | ], 40 | 41 | 'api' => [ 42 | 'throttle:60,1', 43 | 'bindings', 44 | ], 45 | ]; 46 | 47 | /** 48 | * The application's route middleware. 49 | * 50 | * These middleware may be assigned to groups or used individually. 51 | * 52 | * @var array 53 | */ 54 | protected $routeMiddleware = [ 55 | 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 56 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 57 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 58 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 59 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 60 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 61 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 62 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 63 | 'cors' => \App\Http\Middleware\Cors::class, 64 | 'maintenance' => \App\Http\Middleware\CheckMaintenance::class, 65 | 'xRequestId' => \App\Http\Middleware\XRequestId::class, 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckForMaintenanceMode.php: -------------------------------------------------------------------------------- 1 | path() !== 'api/statuses' && $request->method() !== 'OPTIONS' && config('app.maintenance') === true) { 24 | throw new MaintenanceException('サービスはメンテナンス中です。'); 25 | } 26 | return $next($request); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/Cors.php: -------------------------------------------------------------------------------- 1 | header('Access-Control-Allow-Origin', env('CORS_ORIGIN')) 20 | ->header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS') 21 | ->header('Access-Control-Allow-Headers', 'Content-Type, Authorization') 22 | ->header('Access-Control-Expose-Headers', 'Link, Total-Count'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | getPathInfo() === '/api/statuses') { 21 | return $response; 22 | } 23 | 24 | if (!$response->exception) { 25 | app('log')->info( 26 | $request, 27 | [ 28 | 'response' => [ 29 | 'header' => $response->headers->all(), 30 | 'body' => $response->original, 31 | ] 32 | ] 33 | ); 34 | } 35 | 36 | return $response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 21 | return redirect('/home'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | getTraceId(); 25 | 26 | return $next($request) 27 | ->header('X-Request-Id', $XRequestId); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Infrastructure/Logger.php: -------------------------------------------------------------------------------- 1 | setChannel('qiita-stocker-backend'); 24 | $builder->setLogLevel(JsonLogger::toMonologLevel($config['level'])); 25 | $builder->setSkipClassesPartials(['Illuminate\\']); 26 | $builder->setSlackHandler($slackHandlerBuilder->build()); 27 | 28 | // テストコード実行時は標準出力すると見にくいのでファイル出力する 29 | if ($config['env'] === 'testing') { 30 | $builder->setFileName(storage_path('logs/qiita-stocker-backend-' . php_sapi_name() . '.log')); 31 | $builder->setMaxFiles($config['days']); 32 | } else { 33 | $builder->setUseInDocker(true); 34 | } 35 | 36 | return $builder->build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Infrastructure/Repositories/Api/QiitaApiRepository.php: -------------------------------------------------------------------------------- 1 | requestToStockApi($accountEntity->getUserName(), $accountEntity->getAccessToken(), $page, $perPage); 32 | 33 | $responseArray = json_decode($response->getBody()); 34 | 35 | $stockTotalCount = $response->getHeader('total-count'); 36 | 37 | $stockValues = []; 38 | foreach ($responseArray as $stock) { 39 | $stockValue = $this->buildStockValue($stock); 40 | array_push($stockValues, $stockValue); 41 | } 42 | 43 | return new FetchStockValues($stockTotalCount[0], ...$stockValues); 44 | } 45 | 46 | /** 47 | * Stock APIにリクエストを行う 48 | * 49 | * @param string $qiitaUserName 50 | * @param string $accessToken 51 | * @param int $page 52 | * @param int $perPage 53 | * @return mixed|\Psr\Http\Message\ResponseInterface 54 | * @throws \GuzzleHttp\Exception\GuzzleException 55 | */ 56 | private function requestToStockApi(string $qiitaUserName, string $accessToken, int $page, int $perPage) 57 | { 58 | $uri = sprintf( 59 | 'https://qiita.com/api/v2/users/%s/stocks?page=%d&per_page=%d', 60 | $qiitaUserName, 61 | $page, 62 | $perPage 63 | ); 64 | 65 | return $this->getClient()->request( 66 | 'GET', 67 | $uri, 68 | ['headers' => ['Authorization' => 'Bearer '. $accessToken]] 69 | ); 70 | } 71 | 72 | /** 73 | * StockValue を作成する 74 | * 75 | * @param object $stock 76 | * @return StockValue 77 | */ 78 | private function buildStockValue(object $stock): StockValue 79 | { 80 | $articleCreatedAt = new \DateTime($stock->created_at); 81 | $tagNames = $this->buildTagNamesArray($stock->tags); 82 | 83 | $stockValueBuilder = new StockValueBuilder(); 84 | $stockValueBuilder->setArticleId($stock->id); 85 | $stockValueBuilder->setTitle($stock->title); 86 | $stockValueBuilder->setUserId($stock->user->id); 87 | $stockValueBuilder->setProfileImageUrl($stock->user->profile_image_url); 88 | $stockValueBuilder->setArticleCreatedAt($articleCreatedAt); 89 | $stockValueBuilder->setTags($tagNames); 90 | 91 | return $stockValueBuilder->build(); 92 | } 93 | 94 | /** 95 | * タグ名の配列を取得する 96 | * 97 | * @param array $tags 98 | * @return array 99 | */ 100 | private function buildTagNamesArray(array $tags): array 101 | { 102 | $tagNames = []; 103 | foreach ($tags as $tag) { 104 | $tagName = $tag->name; 105 | array_push($tagNames, $tagName); 106 | } 107 | return $tagNames; 108 | } 109 | 110 | /** 111 | * ArticleIDのリストからアイテム一覧を取得する 112 | * 113 | * @param AccountEntity $accountEntity 114 | * @param array $stockArticleIdList 115 | * @return StockValues 116 | */ 117 | public function fetchItemsByArticleIds(AccountEntity $accountEntity, array $stockArticleIdList): StockValues 118 | { 119 | $promises = []; 120 | foreach ($stockArticleIdList as $articleId) { 121 | $uri = sprintf('https://qiita.com/api/v2/items/%s', $articleId); 122 | $promises[] = $this->getClient()->requestAsync( 123 | 'GET', 124 | $uri, 125 | ['headers' => ['Authorization' => 'Bearer '. $accountEntity->getAccessToken()]] 126 | ); 127 | } 128 | 129 | $responses = \GuzzleHttp\Promise\all($promises)->wait(); 130 | 131 | $stockValues = []; 132 | foreach ($responses as $response) { 133 | $stock = json_decode($response->getBody()); 134 | $stockValue = $this->buildStockValue($stock); 135 | array_push($stockValues, $stockValue); 136 | } 137 | 138 | return new StockValues(...$stockValues); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/Infrastructure/Repositories/Api/Repository.php: -------------------------------------------------------------------------------- 1 | client = $client; 28 | } 29 | 30 | /** 31 | * @return Client 32 | */ 33 | final public function getClient(): Client 34 | { 35 | return $this->client; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Infrastructure/Repositories/Eloquent/LoginSessionRepository.php: -------------------------------------------------------------------------------- 1 | id = $loginSessionEntity->getSessionId(); 28 | $loginSession->account_id = $loginSessionEntity->getAccountId(); 29 | $loginSession->expired_on = $loginSessionEntity->getExpiredOn(); 30 | 31 | $loginSession->save(); 32 | } 33 | 34 | /** 35 | * LoginSessionEntityを取得する 36 | * 37 | * @param string $sessionId 38 | * @return LoginSessionEntity 39 | */ 40 | public function find(string $sessionId): LoginSessionEntity 41 | { 42 | $loginSession = LoginSession::findOrFail($sessionId); 43 | 44 | $builder = new LoginSessionEntityBuilder(); 45 | $builder->setSessionId($loginSession->id); 46 | $builder->setAccountId($loginSession->account_id); 47 | $builder->setExpiredOn(new \DateTime($loginSession->expired_on)); 48 | 49 | return $builder->build(); 50 | } 51 | 52 | /** 53 | * アカウントに紐づくログインセッションを削除する 54 | * 55 | * @param string $accountId 56 | */ 57 | public function destroyByAccountId(string $accountId) 58 | { 59 | LoginSession::where('account_id', $accountId)->delete(); 60 | } 61 | 62 | /** 63 | * ログインセッションを削除する 64 | * 65 | * @param string $sessionId 66 | */ 67 | public function destroy(string $sessionId) 68 | { 69 | LoginSession::find($sessionId)->delete(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Models/Domain/Account/AccountEntityBuilder.php: -------------------------------------------------------------------------------- 1 | accountId; 48 | } 49 | 50 | /** 51 | * @param string $accountId 52 | */ 53 | public function setAccountId(string $accountId): void 54 | { 55 | $this->accountId = $accountId; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getPermanentId(): string 62 | { 63 | return $this->permanentId; 64 | } 65 | 66 | /** 67 | * @param string $permanentId 68 | */ 69 | public function setPermanentId(string $permanentId): void 70 | { 71 | $this->permanentId = $permanentId; 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function getUserName(): string 78 | { 79 | return $this->userName; 80 | } 81 | 82 | /** 83 | * @param string $userName 84 | */ 85 | public function setUserName(string $userName): void 86 | { 87 | $this->userName = $userName; 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | public function getAccessToken(): string 94 | { 95 | return $this->accessToken; 96 | } 97 | 98 | /** 99 | * @param string $accessToken 100 | */ 101 | public function setAccessToken(string $accessToken): void 102 | { 103 | $this->accessToken = $accessToken; 104 | } 105 | 106 | /** 107 | * @return AccountEntity 108 | */ 109 | public function build(): AccountEntity 110 | { 111 | return new AccountEntity($this); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/Models/Domain/Account/AccountRepository.php: -------------------------------------------------------------------------------- 1 | 'required|regex:/^[a-z0-9]+$/|min:40|max:64', 24 | 'permanentId' => 'required|integer|min:1|max:4294967294', 25 | 'qiitaAccountId' => 'required|min:1|max:191', 26 | ]); 27 | 28 | if ($validator->fails()) { 29 | return $validator->errors()->toArray(); 30 | } 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryEntities.php: -------------------------------------------------------------------------------- 1 | categoryEntities = $categoryEntities; 27 | } 28 | 29 | /** 30 | * @return CategoryEntity[] 31 | */ 32 | public function getCategoryEntities(): array 33 | { 34 | return $this->categoryEntities; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryEntity.php: -------------------------------------------------------------------------------- 1 | Id = $builder->getId(); 38 | $this->categoryNameValue = $builder->getCategoryNameValue(); 39 | } 40 | 41 | /** 42 | * @return int 43 | */ 44 | public function getId(): int 45 | { 46 | return $this->Id; 47 | } 48 | 49 | /** 50 | * @return CategoryNameValue 51 | */ 52 | public function getCategoryNameValue(): CategoryNameValue 53 | { 54 | return $this->categoryNameValue; 55 | } 56 | 57 | /** 58 | * ストックをカテゴライズする 59 | * 60 | * @param CategoryRepository $categoryRepository 61 | * @param QiitaApiRepository $qiitaApiRepository 62 | * @param AccountEntity $accountEntity 63 | * @param array $articleIds 64 | */ 65 | public function categorize( 66 | CategoryRepository $categoryRepository, 67 | QiitaApiRepository $qiitaApiRepository, 68 | AccountEntity $accountEntity, 69 | array $articleIds 70 | ) { 71 | $this->destroyRelation($categoryRepository, $accountEntity, $articleIds); 72 | $this->createRelation($categoryRepository, $qiitaApiRepository, $articleIds, $accountEntity); 73 | } 74 | 75 | /** 76 | * カテゴリが持つストックのリストを取得する 77 | * 78 | * @param CategoryRepository $categoryRepository 79 | * @param int $limit 80 | * @param int $offset 81 | * @return CategoryStockEntities 82 | */ 83 | public function searchHasCategoryStockEntities(CategoryRepository $categoryRepository, $limit = null, $offset = 0): CategoryStockEntities 84 | { 85 | return $categoryRepository->searchCategoriesStocksByCategoryId($this, $limit, $offset); 86 | } 87 | 88 | /** 89 | * 既存のストックとその他のカテゴリとのリレーションを削除する 90 | * 91 | * @param CategoryRepository $categoryRepository 92 | * @param AccountEntity $accountEntity 93 | * @param array $articleIds 94 | */ 95 | private function destroyRelation(CategoryRepository $categoryRepository, AccountEntity $accountEntity, array $articleIds) 96 | { 97 | $categorizedArticleIds = $categoryRepository->searchCategoriesStocksByArticleId($accountEntity, $this, $articleIds); 98 | $categoryRepository->destroyCategoriesStocks($categorizedArticleIds); 99 | } 100 | 101 | /** 102 | * カテゴリとストックのリレーションを作成する 103 | * 104 | * @param CategoryRepository $categoryRepository 105 | * @param QiitaApiRepository $qiitaApiRepository 106 | * @param array $articleIds 107 | * @param AccountEntity $accountEntity 108 | */ 109 | private function createRelation( 110 | CategoryRepository $categoryRepository, 111 | QiitaApiRepository $qiitaApiRepository, 112 | array $articleIds, 113 | AccountEntity $accountEntity 114 | ) { 115 | $categoryStockEntities = $this->searchHasCategoryStockEntities($categoryRepository); 116 | $stockArticleIdList = $categoryStockEntities->buildArticleIdList(); 117 | 118 | $saveArticleIds = []; 119 | foreach ($articleIds as $articleId) { 120 | if (!in_array($articleId, $stockArticleIdList)) { 121 | array_push($saveArticleIds, $articleId); 122 | } 123 | } 124 | 125 | $uniqueSaveArticleIds = array_unique($saveArticleIds); 126 | if ($uniqueSaveArticleIds) { 127 | $stockValues = $qiitaApiRepository->fetchItemsByArticleIds($accountEntity, $uniqueSaveArticleIds); 128 | 129 | $categoryRepository->createCategoriesStocks($this, $stockValues); 130 | } 131 | } 132 | 133 | /** 134 | * カテゴリIDのバリデーションエラー時に利用するメッセージ 135 | * 136 | * @return string 137 | */ 138 | public static function categoryIdValidationErrorMessage(): string 139 | { 140 | return '不正なリクエストが行われました。'; 141 | } 142 | 143 | /** 144 | * カテゴリが作成されていなかった場合に使用するメッセージ 145 | * 146 | * @return string 147 | */ 148 | public static function categoryNotFoundMessage(): string 149 | { 150 | return '不正なリクエストが行われました。'; 151 | } 152 | 153 | /** 154 | * カテゴリが作成されていなかった場合に使用するメッセージ 155 | * 156 | * @return string 157 | */ 158 | public static function createCategoriesStocksValidationErrorMessage(): string 159 | { 160 | return '不正なリクエストが行われました。'; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryEntityBuilder.php: -------------------------------------------------------------------------------- 1 | Id; 34 | } 35 | 36 | /** 37 | * @param int $Id 38 | */ 39 | public function setId(int $Id): void 40 | { 41 | $this->Id = $Id; 42 | } 43 | 44 | /** 45 | * @return CategoryNameValue 46 | */ 47 | public function getCategoryNameValue(): CategoryNameValue 48 | { 49 | return $this->categoryNameValue; 50 | } 51 | 52 | /** 53 | * @param CategoryNameValue $categoryNameValue 54 | */ 55 | public function setCategoryNameValue(CategoryNameValue $categoryNameValue): void 56 | { 57 | $this->categoryNameValue = $categoryNameValue; 58 | } 59 | 60 | /** 61 | * @return CategoryEntity 62 | */ 63 | public function build(): CategoryEntity 64 | { 65 | return new CategoryEntity($this); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryNameValue.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getName(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | /** 39 | * カテゴリ名のバリデーションエラー時に使用するメッセージ 40 | * 41 | * @return string 42 | */ 43 | public static function nameValidationErrorMessage(): string 44 | { 45 | return 'カテゴリ名は最大50文字です。カテゴリ名を短くしてください。'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryRepository.php: -------------------------------------------------------------------------------- 1 | 'required|max:50', 24 | ]); 25 | 26 | if ($validator->fails()) { 27 | return $validator->errors()->toArray(); 28 | } 29 | return []; 30 | } 31 | 32 | /** 33 | * CategoryEntity が作成可能か確認する 34 | * 35 | * @param array $requestArray 36 | * @return array 37 | */ 38 | public static function canSetCategoryEntityId(array $requestArray): array 39 | { 40 | $validator = \Validator::make($requestArray, [ 41 | 'id' => 'required|integer|min:1' 42 | ]); 43 | 44 | if ($validator->fails()) { 45 | return $validator->errors()->toArray(); 46 | } 47 | return []; 48 | } 49 | 50 | /** 51 | * CategoryEntity が作成可能か確認する 52 | * 53 | * @param array $requestArray 54 | * @return array 55 | */ 56 | public static function canCreateCategoriesStocks(array $requestArray): array 57 | { 58 | $validator = \Validator::make($requestArray, [ 59 | 'articleIds' => 'required|array|max:20', 60 | 'articleIds.*' => 'required|regex:/^[0-9a-f]+$/|min:20|max:20' 61 | ]); 62 | 63 | if ($validator->fails()) { 64 | return $validator->errors()->toArray(); 65 | } 66 | return []; 67 | } 68 | 69 | /** 70 | * カテゴリとストックのリレーションが削除可能か確認する 71 | * 72 | * @param array $requestArray 73 | * @return array 74 | */ 75 | public static function canDestroyRelation(array $requestArray): array 76 | { 77 | $validator = \Validator::make($requestArray, [ 78 | 'id' => 'required|integer|min:1' 79 | ]); 80 | 81 | if ($validator->fails()) { 82 | return $validator->errors()->toArray(); 83 | } 84 | return []; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryStockEntities.php: -------------------------------------------------------------------------------- 1 | categoryStockEntities = $categoryStockEntities; 27 | } 28 | 29 | /** 30 | * @return CategoryStockEntity[] 31 | */ 32 | public function getCategoryStockEntities(): array 33 | { 34 | return $this->categoryStockEntities; 35 | } 36 | 37 | /** 38 | * ArticleIDリストを生成する 39 | * 40 | * @return array 41 | */ 42 | public function buildArticleIdList(): array 43 | { 44 | $categoryStockEntityList = $this->getCategoryStockEntities(); 45 | 46 | $stockArticleIdList = []; 47 | foreach ($categoryStockEntityList as $categoryStockEntity) { 48 | array_push($stockArticleIdList, $categoryStockEntity->getStockValue()->getArticleId()); 49 | } 50 | return $stockArticleIdList; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryStockEntity.php: -------------------------------------------------------------------------------- 1 | id = $builder->getId(); 40 | $this->categoryId = $builder->getCategoryId(); 41 | $this->stockValue = $builder->getStockValue(); 42 | } 43 | 44 | /** 45 | * @return int 46 | */ 47 | public function getId(): int 48 | { 49 | return $this->id; 50 | } 51 | 52 | /** 53 | * @return int 54 | */ 55 | public function getCategoryId(): int 56 | { 57 | return $this->categoryId; 58 | } 59 | 60 | /** 61 | * @return StockValue 62 | */ 63 | public function getStockValue(): StockValue 64 | { 65 | return $this->stockValue; 66 | } 67 | 68 | /** 69 | * カテゴリとストックのリレーションが作成されていなかった場合に使用するメッセージ 70 | * 71 | * @return string 72 | */ 73 | public static function categoryStockNotFoundMessage(): string 74 | { 75 | return '不正なリクエストが行われました。'; 76 | } 77 | 78 | /** 79 | * IDのバリデーションエラー時に利用するメッセージ 80 | * 81 | * @return string 82 | */ 83 | public static function idValidationErrorMessage(): string 84 | { 85 | return '不正なリクエストが行われました。'; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/Models/Domain/Category/CategoryStockEntityBuilder.php: -------------------------------------------------------------------------------- 1 | id; 43 | } 44 | 45 | /** 46 | * @param int $id 47 | */ 48 | public function setId(int $id): void 49 | { 50 | $this->id = $id; 51 | } 52 | 53 | /** 54 | * @return int 55 | */ 56 | public function getCategoryId(): int 57 | { 58 | return $this->categoryId; 59 | } 60 | 61 | /** 62 | * @param int $categoryId 63 | */ 64 | public function setCategoryId(int $categoryId): void 65 | { 66 | $this->categoryId = $categoryId; 67 | } 68 | 69 | /** 70 | * @return StockValue 71 | */ 72 | public function getStockValue(): StockValue 73 | { 74 | return $this->stockValue; 75 | } 76 | 77 | /** 78 | * @param StockValue $stockValue 79 | */ 80 | public function setStockValue(StockValue $stockValue): void 81 | { 82 | $this->stockValue = $stockValue; 83 | } 84 | 85 | public function build() 86 | { 87 | return new CategoryStockEntity($this); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/Models/Domain/ErrorResponseEntity.php: -------------------------------------------------------------------------------- 1 | errorMessage = $builder->getErrorMessage(); 42 | $this->errorCode = $builder->getErrorCode(); 43 | $this->errors = $builder->getErrors(); 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getErrorMessage(): string 50 | { 51 | return $this->errorMessage; 52 | } 53 | 54 | /** 55 | * @return int 56 | */ 57 | public function getErrorCode(): int 58 | { 59 | return $this->errorCode; 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function getErrors(): array 66 | { 67 | return $this->errors; 68 | } 69 | 70 | /** 71 | * レスポンスボディを作成する 72 | * 73 | * @return array 74 | */ 75 | public function buildBody(): array 76 | { 77 | $data = [ 78 | 'code' => $this->getErrorCode(), 79 | 'message' => $this->getErrorMessage(), 80 | ]; 81 | 82 | if ($this->getErrors() !== []) { 83 | $data['errors'] = $this->getErrors(); 84 | } 85 | 86 | return $data; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/Models/Domain/ErrorResponseEntityBuilder.php: -------------------------------------------------------------------------------- 1 | errors = []; 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getErrorMessage(): string 43 | { 44 | return $this->errorMessage; 45 | } 46 | 47 | /** 48 | * @param string $errorMessage 49 | */ 50 | public function setErrorMessage(string $errorMessage): void 51 | { 52 | $this->errorMessage = $errorMessage; 53 | } 54 | 55 | /** 56 | * @return int 57 | */ 58 | public function getErrorCode(): int 59 | { 60 | return $this->errorCode; 61 | } 62 | 63 | /** 64 | * @param int $errorCode 65 | */ 66 | public function setErrorCode(int $errorCode): void 67 | { 68 | $this->errorCode = $errorCode; 69 | } 70 | 71 | /** 72 | * @return array 73 | */ 74 | public function getErrors(): array 75 | { 76 | return $this->errors; 77 | } 78 | 79 | /** 80 | * @param array $errors 81 | */ 82 | public function setErrors(array $errors): void 83 | { 84 | $this->errors = $errors; 85 | } 86 | 87 | /** 88 | * @return ErrorResponseEntity 89 | */ 90 | public function build(): ErrorResponseEntity 91 | { 92 | return new ErrorResponseEntity($this); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Models/Domain/Exceptions/AccountCreatedException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function getErrors(): array 52 | { 53 | return $this->errors; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Models/Domain/LoginSession/LoginSessionEntity.php: -------------------------------------------------------------------------------- 1 | accountId = $builder->getAccountId(); 45 | $this->sessionId = $builder->getSessionId(); 46 | $this->expiredOn = $builder->getExpiredOn(); 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getAccountId(): string 53 | { 54 | return $this->accountId; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getSessionId(): string 61 | { 62 | return $this->sessionId; 63 | } 64 | 65 | /** 66 | * @return \DateTime 67 | */ 68 | public function getExpiredOn(): \DateTime 69 | { 70 | return $this->expiredOn; 71 | } 72 | 73 | /** 74 | * 有効期限が切れているか確認する 75 | * 76 | * @return bool 77 | */ 78 | public function isExpired(): bool 79 | { 80 | $expiredOn = $this->getExpiredOn(); 81 | $now = new \DateTime(); 82 | 83 | if ($expiredOn > $now) { 84 | return false; 85 | } 86 | return true; 87 | } 88 | 89 | /** 90 | * ログインセッションが持つアカウントのAccountEntityを取得する 91 | * 92 | * @param AccountRepository $accountRepository 93 | * @return AccountEntity 94 | */ 95 | public function findHasAccountEntity(AccountRepository $accountRepository): AccountEntity 96 | { 97 | return $accountRepository->find($this->getAccountId()); 98 | } 99 | 100 | /** 101 | * ログインセッションの有効期限が切れている場合のエラーメッセージ 102 | * 103 | * @return string 104 | */ 105 | public function loginSessionExpiredMessage(): string 106 | { 107 | return 'セッションの期限が切れました。再度、ログインしてください。'; 108 | } 109 | 110 | /** 111 | * ログインセッションが不正だった場合のエラーメッセージ 112 | * 113 | * @return string 114 | */ 115 | public static function loginSessionUnauthorizedMessage(): string 116 | { 117 | return 'セッションが不正です。再度、ログインしてください。'; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/Models/Domain/LoginSession/LoginSessionEntityBuilder.php: -------------------------------------------------------------------------------- 1 | accountId; 41 | } 42 | 43 | /** 44 | * @param string $accountId 45 | */ 46 | public function setAccountId(string $accountId): void 47 | { 48 | $this->accountId = $accountId; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getSessionId(): string 55 | { 56 | return $this->sessionId; 57 | } 58 | 59 | /** 60 | * @param string $sessionId 61 | */ 62 | public function setSessionId(string $sessionId): void 63 | { 64 | $this->sessionId = $sessionId; 65 | } 66 | 67 | /** 68 | * @return \DateTime 69 | */ 70 | public function getExpiredOn(): \DateTime 71 | { 72 | return $this->expiredOn; 73 | } 74 | 75 | /** 76 | * @param \DateTime $expiredOn 77 | */ 78 | public function setExpiredOn(\DateTime $expiredOn): void 79 | { 80 | $this->expiredOn = $expiredOn; 81 | } 82 | 83 | /** 84 | * @return LoginSessionEntity 85 | */ 86 | public function build(): LoginSessionEntity 87 | { 88 | return new LoginSessionEntity($this); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/Models/Domain/LoginSession/LoginSessionRepository.php: -------------------------------------------------------------------------------- 1 | 'required|regex:/^[a-z0-9]+$/|min:40|max:64', 24 | 'permanentId' => 'required|integer|min:1|max:4294967294', 25 | 'qiitaAccountId' => 'required|min:1|max:191', 26 | ]); 27 | 28 | if ($validator->fails()) { 29 | return $validator->errors()->toArray(); 30 | } 31 | return []; 32 | } 33 | 34 | /** 35 | * ログインセッションの有効期限を取得する 36 | * 37 | * @return \DateTime 38 | * @throws \Exception 39 | */ 40 | public static function loginSessionExpiration(): \DateTime 41 | { 42 | $expiredOn = new \DateTime(); 43 | 44 | return $expiredOn->add(new \DateInterval('P30D')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Models/Domain/QiitaAccountValue.php: -------------------------------------------------------------------------------- 1 | permanentId = $builder->getPermanentId(); 46 | $this->accessToken = $builder->getAccessToken(); 47 | $this->userName = $builder->getUserName(); 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getPermanentId(): string 54 | { 55 | return $this->permanentId; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getUserName(): string 62 | { 63 | return $this->userName; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getAccessToken(): string 70 | { 71 | return $this->accessToken; 72 | } 73 | 74 | /** 75 | * permanentIDからAccountEntityを取得する 76 | * 77 | * @param AccountRepository $accountRepository 78 | * @return AccountEntity 79 | */ 80 | public function findAccountEntityByPermanentId(AccountRepository $accountRepository): AccountEntity 81 | { 82 | try { 83 | return $accountRepository->findByPermanentId($this); 84 | } catch (ModelNotFoundException $e) { 85 | throw new \RuntimeException(); 86 | } 87 | } 88 | 89 | /** 90 | * アカウントが作成済みか確認する 91 | * 92 | * @param AccountRepository $accountRepository 93 | * @return bool 94 | */ 95 | public function isCreatedAccount(AccountRepository $accountRepository): bool 96 | { 97 | try { 98 | $accountRepository->findByPermanentId($this); 99 | return true; 100 | } catch (ModelNotFoundException $e) { 101 | return false; 102 | } 103 | } 104 | 105 | /** 106 | * アカウント作成時にバリデーションエラーの場合に使用するメッセージ 107 | * 108 | * @return string 109 | */ 110 | public static function createAccountValidationErrorMessage(): string 111 | { 112 | return '不正なリクエストが行われました。再度、アカウント登録を行なってください。'; 113 | } 114 | 115 | /** 116 | * ログイン時にバリデーションエラーの場合に使用するメッセージ 117 | * 118 | * @return string 119 | */ 120 | public static function createLoginSessionValidationErrorMessage(): string 121 | { 122 | return '不正なリクエストが行われました。再度、ログインしてください。'; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/Models/Domain/QiitaAccountValueBuilder.php: -------------------------------------------------------------------------------- 1 | permanentId; 41 | } 42 | 43 | /** 44 | * @param string $permanentId 45 | */ 46 | public function setPermanentId(string $permanentId): void 47 | { 48 | $this->permanentId = $permanentId; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getUserName(): string 55 | { 56 | return $this->userName; 57 | } 58 | 59 | /** 60 | * @param string $userName 61 | */ 62 | public function setUserName(string $userName): void 63 | { 64 | $this->userName = $userName; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getAccessToken(): string 71 | { 72 | return $this->accessToken; 73 | } 74 | 75 | /** 76 | * @param string $accessToken 77 | */ 78 | public function setAccessToken(string $accessToken): void 79 | { 80 | $this->accessToken = $accessToken; 81 | } 82 | 83 | /** 84 | * @return QiitaAccountValue 85 | */ 86 | public function build(): QiitaAccountValue 87 | { 88 | return new QiitaAccountValue($this); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/Models/Domain/QiitaApiRepository.php: -------------------------------------------------------------------------------- 1 | totalCount = $totalCount; 36 | $this->stockValues = $stockValues; 37 | } 38 | 39 | /** 40 | * @return StockValue[] 41 | */ 42 | public function getStockValues(): array 43 | { 44 | return $this->stockValues; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getTotalCount(): int 51 | { 52 | return $this->totalCount; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Models/Domain/Stock/LinkHeaderService.php: -------------------------------------------------------------------------------- 1 | 0; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Models/Domain/Stock/LinkHeaderValue.php: -------------------------------------------------------------------------------- 1 | uriBase = $uriBase; 52 | $this->page = $page; 53 | $this->perPage = $perPage; 54 | $this->relation = $relation; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getUriBase(): string 61 | { 62 | return $this->uriBase; 63 | } 64 | 65 | /** 66 | * @return int 67 | */ 68 | public function getPage(): int 69 | { 70 | return $this->page; 71 | } 72 | 73 | /** 74 | * @return int 75 | */ 76 | public function getPerPage(): int 77 | { 78 | return $this->perPage; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getRelation(): string 85 | { 86 | return $this->relation; 87 | } 88 | 89 | /** 90 | * Linkを作成する 91 | * 92 | * @return string 93 | */ 94 | public function buildLink(): string 95 | { 96 | $uri = sprintf( 97 | '%s?page=%d&per_page=%d', 98 | $this->getUriBase(), 99 | $this->getPage(), 100 | $this->getPerPage() 101 | ); 102 | 103 | return sprintf('<%s>; rel="%s"', $uri, $this->getRelation()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/Models/Domain/Stock/StockSpecification.php: -------------------------------------------------------------------------------- 1 | 'required|integer|min:1|max:100', 24 | 'perPage' => 'required|integer|min:1|max:100', 25 | ]); 26 | 27 | if ($validator->fails()) { 28 | return $validator->errors()->toArray(); 29 | } 30 | return []; 31 | } 32 | 33 | 34 | /** 35 | * カテゴライズ済みのストックが検索可能か確認する 36 | * 37 | * @param array $requestArray 38 | * @return array 39 | */ 40 | public static function canFetchCategorizedStocks(array $requestArray): array 41 | { 42 | $validator = \Validator::make($requestArray, [ 43 | 'page' => 'required|integer|min:1|max:100', 44 | 'perPage' => 'required|integer|min:1|max:100', 45 | 'id' => 'required|integer|min:1' 46 | ]); 47 | 48 | if ($validator->fails()) { 49 | return $validator->errors()->toArray(); 50 | } 51 | return []; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Models/Domain/Stock/StockValue.php: -------------------------------------------------------------------------------- 1 | articleId = $builder->getArticleId(); 63 | $this->title = $builder->getTitle(); 64 | $this->userId = $builder->getUserId(); 65 | $this->profileImageUrl = $builder->getProfileImageUrl(); 66 | $this->articleCreatedAt = $builder->getArticleCreatedAt(); 67 | $this->tags = $builder->getTags(); 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getArticleId(): string 74 | { 75 | return $this->articleId; 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | public function getTitle(): string 82 | { 83 | return $this->title; 84 | } 85 | 86 | /** 87 | * @return string 88 | */ 89 | public function getUserId(): string 90 | { 91 | return $this->userId; 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getProfileImageUrl(): string 98 | { 99 | return $this->profileImageUrl; 100 | } 101 | 102 | /** 103 | * @return \DateTime 104 | */ 105 | public function getArticleCreatedAt(): \DateTime 106 | { 107 | return $this->articleCreatedAt; 108 | } 109 | 110 | /** 111 | * @return string[] 112 | */ 113 | public function getTags(): array 114 | { 115 | return $this->tags; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/Models/Domain/Stock/StockValueBuilder.php: -------------------------------------------------------------------------------- 1 | articleId; 62 | } 63 | 64 | /** 65 | * @param string $articleId 66 | */ 67 | public function setArticleId(string $articleId): void 68 | { 69 | $this->articleId = $articleId; 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function getTitle(): string 76 | { 77 | return $this->title; 78 | } 79 | 80 | /** 81 | * @param string $title 82 | */ 83 | public function setTitle(string $title): void 84 | { 85 | $this->title = $title; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getUserId(): string 92 | { 93 | return $this->userId; 94 | } 95 | 96 | /** 97 | * @param string $userId 98 | */ 99 | public function setUserId(string $userId): void 100 | { 101 | $this->userId = $userId; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getProfileImageUrl(): string 108 | { 109 | return $this->profileImageUrl; 110 | } 111 | 112 | /** 113 | * @param string $profileImageUrl 114 | */ 115 | public function setProfileImageUrl(string $profileImageUrl): void 116 | { 117 | $this->profileImageUrl = $profileImageUrl; 118 | } 119 | 120 | /** 121 | * @return \DateTime 122 | */ 123 | public function getArticleCreatedAt(): \DateTime 124 | { 125 | return $this->articleCreatedAt; 126 | } 127 | 128 | /** 129 | * @param \DateTime $articleCreatedAt 130 | */ 131 | public function setArticleCreatedAt(\DateTime $articleCreatedAt): void 132 | { 133 | $this->articleCreatedAt = $articleCreatedAt; 134 | } 135 | 136 | /** 137 | * @return string[] 138 | */ 139 | public function getTags(): array 140 | { 141 | return $this->tags; 142 | } 143 | 144 | /** 145 | * @param string[] $tags 146 | */ 147 | public function setTags(array $tags): void 148 | { 149 | $this->tags = $tags; 150 | } 151 | 152 | /** 153 | * @return StockValue 154 | */ 155 | public function build(): StockValue 156 | { 157 | return new StockValue($this); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/Models/Domain/Stock/StockValues.php: -------------------------------------------------------------------------------- 1 | stockValues = $stockValues; 27 | } 28 | 29 | /** 30 | * @return StockValue[] 31 | */ 32 | public function getStockValues(): array 33 | { 34 | return $this->stockValues; 35 | } 36 | 37 | /** 38 | * ストック取得時のバリデーションエラーの場合に使用するメッセージ 39 | * 40 | * @return string 41 | */ 42 | public static function searchStocksErrorMessage(): string 43 | { 44 | return '不正なリクエストが行われました。'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind( 38 | AccountRepository::class, 39 | \App\Infrastructure\Repositories\Eloquent\AccountRepository::class 40 | ); 41 | 42 | $this->app->bind( 43 | LoginSessionRepository::class, 44 | \App\Infrastructure\Repositories\Eloquent\LoginSessionRepository::class 45 | ); 46 | 47 | $this->app->bind( 48 | CategoryRepository::class, 49 | \App\Infrastructure\Repositories\Eloquent\CategoryRepository::class 50 | ); 51 | 52 | $this->app->bind( 53 | QiitaApiRepository::class, 54 | \App\Infrastructure\Repositories\Api\QiitaApiRepository::class 55 | ); 56 | 57 | $this->app->bind( 58 | AccountScenario::class, 59 | function () { 60 | return new AccountScenario( 61 | $this->app->make(AccountRepository::class), 62 | $this->app->make(LoginSessionRepository::class), 63 | $this->app->make(CategoryRepository::class) 64 | ); 65 | } 66 | ); 67 | 68 | $this->app->bind( 69 | LoginSessionScenario::class, 70 | function () { 71 | return new LoginSessionScenario( 72 | $this->app->make(AccountRepository::class), 73 | $this->app->make(LoginSessionRepository::class) 74 | ); 75 | } 76 | ); 77 | 78 | $this->app->bind( 79 | CategoryScenario::class, 80 | function () { 81 | return new CategoryScenario( 82 | $this->app->make(AccountRepository::class), 83 | $this->app->make(LoginSessionRepository::class), 84 | $this->app->make(CategoryRepository::class), 85 | $this->app->make(QiitaApiRepository::class) 86 | ); 87 | } 88 | ); 89 | 90 | $this->app->bind( 91 | StockScenario::class, 92 | function () { 93 | return new StockScenario( 94 | $this->app->make(AccountRepository::class), 95 | $this->app->make(LoginSessionRepository::class), 96 | $this->app->make(QiitaApiRepository::class), 97 | $this->app->make(CategoryRepository::class) 98 | ); 99 | } 100 | ); 101 | 102 | $this->app->bind( 103 | Repository::class, 104 | function () { 105 | return new Repository( 106 | $this->app->make(Client::class) 107 | ); 108 | } 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 16 | ]; 17 | 18 | /** 19 | * Register any authentication / authorization services. 20 | * 21 | * @return void 22 | */ 23 | public function boot() 24 | { 25 | $this->registerPolicies(); 26 | 27 | // 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'App\Listeners\EventListener', 18 | ], 19 | ]; 20 | 21 | /** 22 | * Register any events for your application. 23 | * 24 | * @return void 25 | */ 26 | public function boot() 27 | { 28 | parent::boot(); 29 | 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapApiRoutes(); 39 | 40 | $this->mapWebRoutes(); 41 | 42 | // 43 | } 44 | 45 | /** 46 | * Define the "web" routes for the application. 47 | * 48 | * These routes all receive session state, CSRF protection, etc. 49 | * 50 | * @return void 51 | */ 52 | protected function mapWebRoutes() 53 | { 54 | Route::middleware('web') 55 | ->namespace($this->namespace) 56 | ->group(base_path('routes/web.php')); 57 | } 58 | 59 | /** 60 | * Define the "api" routes for the application. 61 | * 62 | * These routes are typically stateless. 63 | * 64 | * @return void 65 | */ 66 | protected function mapApiRoutes() 67 | { 68 | Route::prefix('api') 69 | ->middleware('api') 70 | ->namespace($this->namespace) 71 | ->group(base_path('routes/api.php')); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Services/Authentication.php: -------------------------------------------------------------------------------- 1 | find($params['sessionId']); 42 | 43 | if ($loginSessionEntity->isExpired()) { 44 | throw new LoginSessionExpiredException($loginSessionEntity->loginSessionExpiredMessage()); 45 | } 46 | 47 | return $loginSessionEntity->findHasAccountEntity($accountRepository); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/User.php: -------------------------------------------------------------------------------- 1 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /buildspec-migration.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - composer install 7 | build: 8 | commands: 9 | - php artisan migrate --force 10 | -------------------------------------------------------------------------------- /buildspec-push-ecr.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | docker: 18 7 | pre_build: 8 | commands: 9 | - echo Logging in to Amazon ECR... 10 | - $(aws ecr get-login --no-include-email --region ap-northeast-1) 11 | - echo ${DEPLOY_STAGE} 12 | - echo ${AWS_ACCOUNT_ID} 13 | - echo ${IMAGE_TAG} 14 | - echo ${REPOSITORY_NGINX} 15 | - echo ${REPOSITORY_PHP} 16 | build: 17 | commands: 18 | - echo Build started on `date` 19 | - echo Building the Docker image... 20 | - docker build --no-cache --rm -t ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NGINX}:latest -f docker/nginx/Dockerfile . 21 | - docker tag ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NGINX}:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NGINX}:${IMAGE_TAG} 22 | - docker build --no-cache --rm -t ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_PHP}:latest -f docker/php/Dockerfile . 23 | - docker tag ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_PHP}:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_PHP}:${IMAGE_TAG} 24 | post_build: 25 | commands: 26 | - echo Build completed on `date` 27 | - echo Pushing the Docker image... 28 | - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NGINX}:latest 29 | - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NGINX}:${IMAGE_TAG} 30 | - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_PHP}:latest 31 | - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_PHP}:${IMAGE_TAG} 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "description": "The Laravel Framework.", 4 | "keywords": ["framework", "laravel"], 5 | "license": "MIT", 6 | "type": "project", 7 | "require": { 8 | "php": "^7.1.3", 9 | "fideloper/proxy": "^4.0", 10 | "guzzlehttp/guzzle": "^6.3", 11 | "laravel/framework": "5.7.*", 12 | "laravel/tinker": "^1.0", 13 | "nekonomokochan/php-json-logger": "^1.3", 14 | "ramsey/uuid": "^3.8" 15 | }, 16 | "require-dev": { 17 | "filp/whoops": "^2.0", 18 | "friendsofphp/php-cs-fixer": "^2.13", 19 | "fzaninotto/faker": "^1.4", 20 | "mockery/mockery": "^1.0", 21 | "nunomaduro/collision": "^2.0", 22 | "phpunit/phpunit": "^7.0" 23 | }, 24 | "autoload": { 25 | "classmap": [ 26 | "database/seeds", 27 | "database/factories" 28 | ], 29 | "psr-4": { 30 | "App\\": "app/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Tests\\": "tests/" 36 | } 37 | }, 38 | "extra": { 39 | "laravel": { 40 | "dont-discover": [ 41 | ] 42 | } 43 | }, 44 | "scripts": { 45 | "post-root-package-install": [ 46 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 47 | ], 48 | "post-create-project-cmd": [ 49 | "@php artisan key:generate" 50 | ], 51 | "post-autoload-dump": [ 52 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 53 | "@php artisan package:discover" 54 | ], 55 | "phpcs": "php-cs-fixer fix --dry-run --diff -v --config .php_cs.dist", 56 | "phpcs:format": "php-cs-fixer fix --diff -v --config .php_cs.dist", 57 | "test": "phpunit", 58 | "test:coverage": "phpunit --coverage-html coverage" 59 | }, 60 | "config": { 61 | "preferred-install": "dist", 62 | "sort-packages": true, 63 | "optimize-autoloader": true 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true 67 | } 68 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'web', 18 | 'passwords' => 'users', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | here which uses session storage and the Eloquent user provider. 29 | | 30 | | All authentication drivers have a user provider. This defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | mechanisms used by this application to persist your user's data. 33 | | 34 | | Supported: "session", "token" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | 44 | 'api' => [ 45 | 'driver' => 'token', 46 | 'provider' => 'users', 47 | ], 48 | ], 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | User Providers 53 | |-------------------------------------------------------------------------- 54 | | 55 | | All authentication drivers have a user provider. This defines how the 56 | | users are actually retrieved out of your database or other storage 57 | | mechanisms used by this application to persist your user's data. 58 | | 59 | | If you have multiple user tables or models you may configure multiple 60 | | sources which represent each model / table. These sources may then 61 | | be assigned to any extra authentication guards you have defined. 62 | | 63 | | Supported: "database", "eloquent" 64 | | 65 | */ 66 | 67 | 'providers' => [ 68 | 'users' => [ 69 | 'driver' => 'eloquent', 70 | 'model' => App\User::class, 71 | ], 72 | 73 | // 'users' => [ 74 | // 'driver' => 'database', 75 | // 'table' => 'users', 76 | // ], 77 | ], 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Resetting Passwords 82 | |-------------------------------------------------------------------------- 83 | | 84 | | You may specify multiple password reset configurations if you have more 85 | | than one user table or model in the application and you want to have 86 | | separate password reset settings based on the specific user types. 87 | | 88 | | The expire time is the number of minutes that the reset token should be 89 | | considered valid. This security feature keeps tokens short-lived so 90 | | they have less time to be guessed. You may change this as needed. 91 | | 92 | */ 93 | 94 | 'passwords' => [ 95 | 'users' => [ 96 | 'provider' => 'users', 97 | 'table' => 'password_resets', 98 | 'expire' => 60, 99 | ], 100 | ], 101 | 102 | ]; 103 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'encrypted' => true, 41 | ], 42 | ], 43 | 44 | 'redis' => [ 45 | 'driver' => 'redis', 46 | 'connection' => 'default', 47 | ], 48 | 49 | 'log' => [ 50 | 'driver' => 'log', 51 | ], 52 | 53 | 'null' => [ 54 | 'driver' => 'null', 55 | ], 56 | 57 | ], 58 | 59 | ]; 60 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | */ 30 | 31 | 'stores' => [ 32 | 33 | 'apc' => [ 34 | 'driver' => 'apc', 35 | ], 36 | 37 | 'array' => [ 38 | 'driver' => 'array', 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'table' => 'cache', 44 | 'connection' => null, 45 | ], 46 | 47 | 'file' => [ 48 | 'driver' => 'file', 49 | 'path' => storage_path('framework/cache/data'), 50 | ], 51 | 52 | 'memcached' => [ 53 | 'driver' => 'memcached', 54 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 55 | 'sasl' => [ 56 | env('MEMCACHED_USERNAME'), 57 | env('MEMCACHED_PASSWORD'), 58 | ], 59 | 'options' => [ 60 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 61 | ], 62 | 'servers' => [ 63 | [ 64 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 65 | 'port' => env('MEMCACHED_PORT', 11211), 66 | 'weight' => 100, 67 | ], 68 | ], 69 | ], 70 | 71 | 'redis' => [ 72 | 'driver' => 'redis', 73 | 'connection' => 'default', 74 | ], 75 | 76 | ], 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Cache Key Prefix 81 | |-------------------------------------------------------------------------- 82 | | 83 | | When utilizing a RAM based store such as APC or Memcached, there might 84 | | be other applications utilizing the same cache. So, we'll specify a 85 | | value to get prefixed to all our keys so we can avoid collisions. 86 | | 87 | */ 88 | 89 | 'prefix' => env( 90 | 'CACHE_PREFIX', 91 | str_slug(env('APP_NAME', 'laravel'), '_').'_cache' 92 | ), 93 | 94 | ]; 95 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'mysql'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Database Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here are each of the database connections setup for your application. 24 | | Of course, examples of configuring each database platform that is 25 | | supported by Laravel is shown below to make development simple. 26 | | 27 | | 28 | | All database work in Laravel is done through the PHP PDO facilities 29 | | so make sure you have the driver for your particular database of 30 | | choice installed on your machine before you begin development. 31 | | 32 | */ 33 | 34 | 'connections' => [ 35 | 36 | 'sqlite' => [ 37 | 'driver' => 'sqlite', 38 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 39 | 'prefix' => '', 40 | ], 41 | 42 | 'mysql' => [ 43 | 'driver' => 'mysql', 44 | 'host' => env('DB_HOST', '127.0.0.1'), 45 | 'port' => env('DB_PORT', '3306'), 46 | 'database' => env('DB_DATABASE', 'forge'), 47 | 'username' => env('DB_USERNAME', 'forge'), 48 | 'password' => env('DB_PASSWORD', ''), 49 | 'unix_socket' => env('DB_SOCKET', ''), 50 | 'charset' => 'utf8mb4', 51 | 'collation' => 'utf8mb4_bin', 52 | 'prefix' => '', 53 | 'strict' => true, 54 | 'engine' => 'InnoDB ROW_FORMAT=DYNAMIC', 55 | ], 56 | 57 | 'pgsql' => [ 58 | 'driver' => 'pgsql', 59 | 'host' => env('DB_HOST', '127.0.0.1'), 60 | 'port' => env('DB_PORT', '5432'), 61 | 'database' => env('DB_DATABASE', 'forge'), 62 | 'username' => env('DB_USERNAME', 'forge'), 63 | 'password' => env('DB_PASSWORD', ''), 64 | 'charset' => 'utf8', 65 | 'prefix' => '', 66 | 'schema' => 'public', 67 | 'sslmode' => 'prefer', 68 | ], 69 | 70 | 'sqlsrv' => [ 71 | 'driver' => 'sqlsrv', 72 | 'host' => env('DB_HOST', 'localhost'), 73 | 'port' => env('DB_PORT', '1433'), 74 | 'database' => env('DB_DATABASE', 'forge'), 75 | 'username' => env('DB_USERNAME', 'forge'), 76 | 'password' => env('DB_PASSWORD', ''), 77 | 'charset' => 'utf8', 78 | 'prefix' => '', 79 | ], 80 | 81 | 'circle_test' => [ 82 | 'driver' => 'mysql', 83 | 'host' => '127.0.0.1', 84 | 'port' => '3306', 85 | 'database' => 'circle_test', 86 | 'username' => 'root', 87 | 'password' => '', 88 | 'charset' => 'utf8mb4', 89 | 'collation' => 'utf8mb4_bin', 90 | 'prefix' => '', 91 | 'strict' => true, 92 | 'engine' => 'InnoDB ROW_FORMAT=DYNAMIC', 93 | ] 94 | ], 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | Migration Repository Table 99 | |-------------------------------------------------------------------------- 100 | | 101 | | This table keeps track of all the migrations that have already run for 102 | | your application. Using this information, we can determine which of 103 | | the migrations on disk haven't actually been run in the database. 104 | | 105 | */ 106 | 107 | 'migrations' => 'migrations', 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | Redis Databases 112 | |-------------------------------------------------------------------------- 113 | | 114 | | Redis is an open source, fast, and advanced key-value store that also 115 | | provides a richer set of commands than a typical key-value systems 116 | | such as APC or Memcached. Laravel makes it easy to dig right in. 117 | | 118 | */ 119 | 120 | 'redis' => [ 121 | 122 | 'client' => 'predis', 123 | 124 | 'default' => [ 125 | 'host' => env('REDIS_HOST', '127.0.0.1'), 126 | 'password' => env('REDIS_PASSWORD', null), 127 | 'port' => env('REDIS_PORT', 6379), 128 | 'database' => 0, 129 | ], 130 | 131 | ], 132 | 133 | ]; 134 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DRIVER', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Cloud Filesystem Disk 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Many applications store files both locally and in the cloud. For this 24 | | reason, you may specify a default "cloud" driver here. This driver 25 | | will be bound as the Cloud disk implementation in the container. 26 | | 27 | */ 28 | 29 | 'cloud' => env('FILESYSTEM_CLOUD', 's3'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Filesystem Disks 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may configure as many filesystem "disks" as you wish, and you 37 | | may even configure multiple disks of the same driver. Defaults have 38 | | been setup for each driver as an example of the required options. 39 | | 40 | | Supported Drivers: "local", "ftp", "sftp", "s3", "rackspace" 41 | | 42 | */ 43 | 44 | 'disks' => [ 45 | 46 | 'local' => [ 47 | 'driver' => 'local', 48 | 'root' => storage_path('app'), 49 | ], 50 | 51 | 'public' => [ 52 | 'driver' => 'local', 53 | 'root' => storage_path('app/public'), 54 | 'url' => env('APP_URL').'/storage', 55 | 'visibility' => 'public', 56 | ], 57 | 58 | 's3' => [ 59 | 'driver' => 's3', 60 | 'key' => env('AWS_ACCESS_KEY_ID'), 61 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 62 | 'region' => env('AWS_DEFAULT_REGION'), 63 | 'bucket' => env('AWS_BUCKET'), 64 | 'url' => env('AWS_URL'), 65 | ], 66 | 67 | ], 68 | 69 | ]; 70 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 1024, 48 | 'threads' => 2, 49 | 'time' => 2, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'app'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Log Channels 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may configure the log channels for your application. Out of 26 | | the box, Laravel uses the Monolog PHP logging library. This gives 27 | | you a variety of powerful log handlers / formatters to utilize. 28 | | 29 | | Available Drivers: "single", "daily", "slack", "syslog", 30 | | "errorlog", "monolog", 31 | | "custom", "stack" 32 | | 33 | */ 34 | 35 | 'channels' => [ 36 | 'app' => [ 37 | 'driver' => 'custom', 38 | 'level' => env('APP_LOG_LEVEL', 'debug'), 39 | 'days' => 10, 40 | 'slack_token' => env('NOTIFICATION_SLACK_TOKEN'), 41 | 'slack_channel' => env('NOTIFICATION_SLACK_CHANNEL'), 42 | 'via' => App\Infrastructure\Logger::class, 43 | 'env' => env('APP_ENV', 'production'), 44 | ], 45 | 46 | 'stack' => [ 47 | 'driver' => 'stack', 48 | 'channels' => ['single'], 49 | ], 50 | 51 | 'single' => [ 52 | 'driver' => 'single', 53 | 'path' => storage_path('logs/laravel.log'), 54 | 'level' => 'debug', 55 | ], 56 | 57 | 'daily' => [ 58 | 'driver' => 'daily', 59 | 'path' => storage_path('logs/laravel.log'), 60 | 'level' => 'debug', 61 | 'days' => 7, 62 | ], 63 | 64 | 'slack' => [ 65 | 'driver' => 'slack', 66 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 67 | 'username' => 'Laravel Log', 68 | 'emoji' => ':boom:', 69 | 'level' => 'critical', 70 | ], 71 | 72 | 'stderr' => [ 73 | 'driver' => 'monolog', 74 | 'handler' => StreamHandler::class, 75 | 'with' => [ 76 | 'stream' => 'php://stderr', 77 | ], 78 | ], 79 | 80 | 'syslog' => [ 81 | 'driver' => 'syslog', 82 | 'level' => 'debug', 83 | ], 84 | 85 | 'errorlog' => [ 86 | 'driver' => 'errorlog', 87 | 'level' => 'debug', 88 | ], 89 | ], 90 | 91 | ]; 92 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_DRIVER', 'smtp'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | SMTP Host Address 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may provide the host address of the SMTP server used by your 27 | | applications. A default option is provided that is compatible with 28 | | the Mailgun mail service which will provide reliable deliveries. 29 | | 30 | */ 31 | 32 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | SMTP Host Port 37 | |-------------------------------------------------------------------------- 38 | | 39 | | This is the SMTP port used by your application to deliver e-mails to 40 | | users of the application. Like the host we have set this value to 41 | | stay compatible with the Mailgun e-mail application by default. 42 | | 43 | */ 44 | 45 | 'port' => env('MAIL_PORT', 587), 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Global "From" Address 50 | |-------------------------------------------------------------------------- 51 | | 52 | | You may wish for all e-mails sent by your application to be sent from 53 | | the same address. Here, you may specify a name and address that is 54 | | used globally for all e-mails that are sent by your application. 55 | | 56 | */ 57 | 58 | 'from' => [ 59 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 60 | 'name' => env('MAIL_FROM_NAME', 'Example'), 61 | ], 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | E-Mail Encryption Protocol 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here you may specify the encryption protocol that should be used when 69 | | the application send e-mail messages. A sensible default using the 70 | | transport layer security protocol should provide great security. 71 | | 72 | */ 73 | 74 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | SMTP Server Username 79 | |-------------------------------------------------------------------------- 80 | | 81 | | If your SMTP server requires a username for authentication, you should 82 | | set it here. This will get used to authenticate with your server on 83 | | connection. You may also set the "password" value below this one. 84 | | 85 | */ 86 | 87 | 'username' => env('MAIL_USERNAME'), 88 | 89 | 'password' => env('MAIL_PASSWORD'), 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Sendmail System Path 94 | |-------------------------------------------------------------------------- 95 | | 96 | | When using the "sendmail" driver to send e-mails, we will need to know 97 | | the path to where Sendmail lives on this server. A default path has 98 | | been provided here, which will work well on most of your systems. 99 | | 100 | */ 101 | 102 | 'sendmail' => '/usr/sbin/sendmail -bs', 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Markdown Mail Settings 107 | |-------------------------------------------------------------------------- 108 | | 109 | | If you are using Markdown based email rendering, you may configure your 110 | | theme and component paths here, allowing you to customize the design 111 | | of the emails. Or, you may simply stick with the Laravel defaults! 112 | | 113 | */ 114 | 115 | 'markdown' => [ 116 | 'theme' => 'default', 117 | 118 | 'paths' => [ 119 | resource_path('views/vendor/mail'), 120 | ], 121 | ], 122 | 123 | ]; 124 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_DRIVER', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | ], 43 | 44 | 'beanstalkd' => [ 45 | 'driver' => 'beanstalkd', 46 | 'host' => 'localhost', 47 | 'queue' => 'default', 48 | 'retry_after' => 90, 49 | ], 50 | 51 | 'sqs' => [ 52 | 'driver' => 'sqs', 53 | 'key' => env('SQS_KEY', 'your-public-key'), 54 | 'secret' => env('SQS_SECRET', 'your-secret-key'), 55 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 56 | 'queue' => env('SQS_QUEUE', 'your-queue-name'), 57 | 'region' => env('SQS_REGION', 'us-east-1'), 58 | ], 59 | 60 | 'redis' => [ 61 | 'driver' => 'redis', 62 | 'connection' => 'default', 63 | 'queue' => 'default', 64 | 'retry_after' => 90, 65 | 'block_for' => null, 66 | ], 67 | 68 | ], 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Failed Queue Jobs 73 | |-------------------------------------------------------------------------- 74 | | 75 | | These options configure the behavior of failed queue job logging so you 76 | | can control which database and table are used to store the jobs that 77 | | have failed. You may change them to any database / table you wish. 78 | | 79 | */ 80 | 81 | 'failed' => [ 82 | 'database' => env('DB_CONNECTION', 'mysql'), 83 | 'table' => 'failed_jobs', 84 | ], 85 | 86 | ]; 87 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | ], 21 | 22 | 'ses' => [ 23 | 'key' => env('SES_KEY'), 24 | 'secret' => env('SES_SECRET'), 25 | 'region' => env('SES_REGION', 'us-east-1'), 26 | ], 27 | 28 | 'sparkpost' => [ 29 | 'secret' => env('SPARKPOST_SECRET'), 30 | ], 31 | 32 | 'stripe' => [ 33 | 'model' => App\User::class, 34 | 'key' => env('STRIPE_KEY'), 35 | 'secret' => env('STRIPE_SECRET'), 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => realpath(storage_path('framework/views')), 32 | 33 | ]; 34 | -------------------------------------------------------------------------------- /createDotenv.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | // TODO 将来的にこのscriptはローカル環境になる、その為には以下の課題解決が必要 3 | // TODO https://github.com/nekochans/qiita-stocker-terraform/issues/74 4 | // TODO https://github.com/nekochans/qiita-stocker-terraform/issues/75 5 | const deployUtils = require("./deployUtils"); 6 | 7 | const deployStage = process.env.DEPLOY_STAGE; 8 | if (deployUtils.isAllowedDeployStage(deployStage) === false) { 9 | return Promise.reject( 10 | new Error( 11 | "有効なステージではありません。local, dev, stg, prod が利用出来ます。" 12 | ) 13 | ); 14 | } 15 | 16 | const awsEnvCreator = require("@nekonomokochan/aws-env-creator"); 17 | 18 | const params = { 19 | type: ".env", 20 | outputDir: "./", 21 | parameterPath: `/${deployStage}/qiita-stocker/api`, 22 | region: "ap-northeast-1", 23 | profile: deployUtils.findAwsProfile(deployStage), 24 | }; 25 | 26 | await awsEnvCreator.createEnvFile(params); 27 | if (deployStage === "local") { 28 | const outputFilename = ".env.testing"; 29 | params["outputFilename"] = outputFilename; 30 | await awsEnvCreator.createEnvFile(params); 31 | 32 | const replaceParams = { 33 | outputFilename: outputFilename, 34 | outputParam: { 35 | DB_DATABASE: "qiita_stocker_test", 36 | DB_USERNAME: "qiita_stocker_test", 37 | APP_ENV: "testing", 38 | }, 39 | }; 40 | deployUtils.replaceEnvFile(replaceParams); 41 | } 42 | })(); 43 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /database/database.sql: -------------------------------------------------------------------------------- 1 | -- 'qiita_stocker' というユーザー名のユーザーを '(YourPassword999)' というパスワードで作成 2 | -- データベース 'qiita_stocker' への権限を付与 3 | CREATE DATABASE qiita_stocker; 4 | CREATE USER qiita_stocker@localhost IDENTIFIED WITH mysql_native_password BY '(YourPassword999)'; 5 | GRANT ALL ON qiita_stocker.* TO 'qiita_stocker'@'localhost'; 6 | 7 | -- 'qiita_stocker_test' というユーザー名のユーザーを '(YourPassword999)' というパスワードで作成 8 | -- データベース 'qiita_stocker_test' への権限を付与 9 | CREATE DATABASE qiita_stocker_test; 10 | CREATE USER qiita_stocker_test@localhost IDENTIFIED WITH mysql_native_password BY '(YourPassword999)'; 11 | GRANT ALL ON qiita_stocker_test.* TO 'qiita_stocker_test'@'localhost'; 12 | -------------------------------------------------------------------------------- /database/factories/AccountFactory.php: -------------------------------------------------------------------------------- 1 | define(App\Eloquents\Account::class, function (Faker $faker) { 6 | return []; 7 | }); 8 | 9 | $factory->define(App\Eloquents\QiitaAccount::class, function (Faker $faker) { 10 | return [ 11 | 'account_id' => '1', 12 | 'qiita_account_id' => '1' 13 | ]; 14 | }); 15 | 16 | $factory->define(App\Eloquents\QiitaUserName::class, function (Faker $faker) { 17 | return [ 18 | 'account_id' => '1', 19 | 'user_name' => $faker->word 20 | ]; 21 | }); 22 | 23 | 24 | $factory->define(App\Eloquents\AccessToken::class, function (Faker $faker) { 25 | return [ 26 | 'account_id' => '1', 27 | 'access_token' => $faker->unique()->regexify('[a-z0-9]{64}') 28 | ]; 29 | }); 30 | 31 | $factory->define(App\Eloquents\LoginSession::class, function (Faker $faker) { 32 | $expiredOn = new \DateTime(); 33 | $expiredOn->add(new \DateInterval('PT1H')); 34 | 35 | return [ 36 | 'id' => $faker->uuid(), 37 | 'account_id' => '1', 38 | 'expired_on' => $expiredOn 39 | ]; 40 | }); 41 | -------------------------------------------------------------------------------- /database/factories/CategoryFactory.php: -------------------------------------------------------------------------------- 1 | define(\App\Eloquents\Category::class, function (Faker $faker) { 6 | return [ 7 | 'account_id' => '1', 8 | ]; 9 | }); 10 | 11 | $factory->define(\App\Eloquents\CategoryName::class, function (Faker $faker) { 12 | return [ 13 | 'category_id' => '1', 14 | 'name' => $faker->word, 15 | ]; 16 | }); 17 | 18 | $factory->define(\App\Eloquents\CategoryStock::class, function (Faker $faker) { 19 | return [ 20 | 'category_id' => '1', 21 | 'article_id' => $faker->unique()->regexify('[a-z0-9]{20}'), 22 | 'title' => $faker->sentence, 23 | 'user_id' => $faker->userName, 24 | 'profile_image_url' => $faker->url, 25 | 'article_created_at' => $faker->dateTimeThisDecade, 26 | 'tags' => json_encode(['testTag']), 27 | ]; 28 | }); 29 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(App\User::class, function (Faker $faker) { 17 | return [ 18 | 'name' => $faker->name, 19 | 'email' => $faker->unique()->safeEmail, 20 | 'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret 21 | 'remember_token' => str_random(10), 22 | ]; 23 | }); 24 | -------------------------------------------------------------------------------- /database/migrations/2018_10_01_174932_create_accounts_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->unsignedInteger('lock_version')->default(0); 20 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 21 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('accounts'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2018_10_02_070100_create_accounts_qiita_accounts_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->unsignedInteger('account_id'); 20 | $table->string('qiita_account_id'); 21 | $table->unsignedInteger('lock_version')->default(0); 22 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 23 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 24 | $table->unique('account_id', 'uq_accounts_qiita_accounts_01'); 25 | $table->unique('qiita_account_id', 'uq_accounts_qiita_accounts_02'); 26 | $table->foreign('account_id', 'fk_accounts_qiita_accounts_01')->references('id')->on('accounts'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('accounts_qiita_accounts'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2018_10_02_070854_create_accounts_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->unsignedInteger('account_id'); 20 | $table->string('access_token'); 21 | $table->unsignedInteger('lock_version')->default(0); 22 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 23 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 24 | $table->unique('account_id', 'uq_accounts_access_tokens_01'); 25 | $table->unique('access_token', 'uq_accounts_access_tokens_02'); 26 | $table->foreign('account_id', 'fk_accounts_access_tokens_01')->references('id')->on('accounts'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('accounts_access_tokens'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2018_10_02_072202_create_login_sessions_table.php: -------------------------------------------------------------------------------- 1 | string('id'); 19 | $table->unsignedInteger('account_id'); 20 | $table->dateTime('expired_on'); 21 | $table->unsignedInteger('lock_version')->default(0); 22 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 23 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 24 | $table->primary('id'); 25 | $table->foreign('account_id', 'fk_login_sessions_01')->references('id')->on('accounts'); 26 | $table->index('account_id', 'idx_login_sessions_01'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('login_sessions'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2018_11_13_002016_create_categories_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('account_id'); 19 | $table->unsignedInteger('lock_version')->default(0); 20 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 21 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 22 | $table->foreign('account_id', 'fk_categories_01')->references('id')->on('accounts'); 23 | $table->index('account_id', 'idx_categories_01'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('categories'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2018_11_13_005301_create_categories_names_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('category_id'); 19 | $table->string('name'); 20 | $table->unsignedInteger('lock_version')->default(0); 21 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 22 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 23 | $table->unique('category_id', 'uq_categories_names_01'); 24 | $table->foreign('category_id', 'fk_categories_names_01')->references('id')->on('categories'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('categories_names'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2018_12_10_004032_create_accounts_qiita_user_names_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('account_id'); 19 | $table->string('user_name'); 20 | $table->unsignedInteger('lock_version')->default(0); 21 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 22 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 23 | $table->unique('account_id', 'uq_accounts_qiita_user_names_01'); 24 | $table->unique('user_name', 'uq_accounts_qiita_user_names_02'); 25 | $table->foreign('account_id', 'fk_accounts_qiita_user_names_01')->references('id')->on('accounts'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('accounts_qiita_user_names'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2018_12_25_114434_create_categories_stocks_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('category_id'); 19 | $table->string('article_id'); 20 | $table->text('title'); 21 | $table->string('user_id'); 22 | $table->text('profile_image_url'); 23 | $table->dateTime('article_created_at'); 24 | $table->text('tags'); 25 | $table->unsignedInteger('lock_version')->default(0); 26 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 27 | $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); 28 | $table->foreign('category_id', 'fk_categories_stocks_01')->references('id')->on('categories'); 29 | $table->index('article_id', 'idx_categories_stocks_01'); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists('categories_stocks'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /deployUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 許可されたデプロイステージかどうか判定する 3 | * 4 | * @param deployStage 5 | * @return {boolean} 6 | */ 7 | exports.isAllowedDeployStage = deployStage => 8 | deployStage === "local" || 9 | deployStage === "dev" || 10 | deployStage === "stg" || 11 | deployStage === "prod"; 12 | 13 | /** 14 | * AWSのプロファイル名を取得する 15 | * 16 | * @return {string} 17 | */ 18 | exports.findAwsProfile = deployStage => { 19 | if (deployStage === "prod") { 20 | return "qiita-stocker-prod"; 21 | } 22 | 23 | return "qiita-stocker-dev"; 24 | }; 25 | 26 | /** 27 | * EnvFileの中身を置換する 28 | * 29 | * @param replaceParams 30 | */ 31 | exports.replaceEnvFile = replaceParams => { 32 | const fs = require("fs"); 33 | let data = fs.readFileSync(replaceParams.outputFilename, "utf-8"); 34 | 35 | for (const [key, value] of Object.entries(replaceParams.outputParam)) { 36 | data = data.replace(new RegExp(`${key}=.*`, "g"), `${key}=${value}`); 37 | } 38 | 39 | fs.unlinkSync(replaceParams.outputFilename); 40 | fs.appendFileSync(replaceParams.outputFilename, data); 41 | }; 42 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | nginx: 4 | build: 5 | context: . 6 | dockerfile: ./docker/nginx/Dockerfile 7 | ports: 8 | - "80:80" 9 | environment: 10 | PHP_HOST: php 11 | depends_on: 12 | - php 13 | php: 14 | build: 15 | context: . 16 | dockerfile: ./docker/php/Dockerfile 17 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.15.5-alpine 2 | 3 | ENV PHP_HOST=127.0.0.1 4 | 5 | ADD ./docker/nginx/config/default.conf.template /etc/nginx/conf.d/default.conf.template 6 | ADD ./docker/nginx/config/nginx.conf /etc/nginx/nginx.conf 7 | 8 | RUN mkdir -p /var/www/html/public 9 | ADD ./public/ /var/www/html/public 10 | 11 | CMD /bin/sh -c 'sed "s/\${PHP_HOST}/$PHP_HOST/" /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g "daemon off;"' 12 | -------------------------------------------------------------------------------- /docker/nginx/config/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | root /var/www/html/public/; 5 | index index.php; 6 | 7 | access_log /var/log/nginx/access.log; 8 | error_log /var/log/nginx/error.log; 9 | 10 | # リクエストサイズの最大値 11 | client_max_body_size 1M; 12 | 13 | location / { 14 | try_files $uri $uri/ /index.php$is_args$args; 15 | } 16 | 17 | location ~ \.php$ { 18 | try_files $uri = 404; 19 | fastcgi_split_path_info ^(.+\.php)(\.+)$; 20 | fastcgi_pass ${PHP_HOST}:9000; 21 | fastcgi_index index.php; 22 | include fastcgi_params; 23 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 24 | fastcgi_param PATH_INFO $fastcgi_path_info; 25 | } 26 | 27 | error_page 404 /index.php; 28 | 29 | #-------------------- 30 | # 静的ファイル設定 31 | #-------------------- 32 | # 画像やCSSにはキャッシュの有効期限を設定&アクセスログOFF 33 | location ~ \.(html|css|js|jpe?g|png|gif|svg|mpg|flv|swf)$ { 34 | expires 10d; 35 | access_log off; 36 | break; 37 | } 38 | 39 | # .gitや.htaccessにはアクセス禁止 40 | location ~ /\.(ht|git|svn) { 41 | deny all; 42 | } 43 | 44 | # faviconへのアクセスは記録しない 45 | location = /favicon.ico { 46 | access_log off; 47 | log_not_found off; 48 | } 49 | 50 | # robots.txtへのアクセスは記録しない 51 | location = /robots.txt { 52 | access_log off; 53 | log_not_found off; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docker/nginx/config/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 65535; 9 | multi_accept on; 10 | use epoll; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | include /etc/nginx/conf.d/*.conf; 31 | } 32 | -------------------------------------------------------------------------------- /docker/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.3.2-fpm-alpine 2 | 3 | COPY . . 4 | WORKDIR /var/www/html 5 | 6 | RUN set -x && \ 7 | apk update && \ 8 | apk add --no-cache libxml2 libxml2-dev curl curl-dev autoconf $PHPIZE_DEPS && \ 9 | docker-php-ext-install opcache mysqli pdo pdo_mysql xml mbstring curl session tokenizer json && \ 10 | curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer && \ 11 | composer global require hirak/prestissimo && \ 12 | composer install && \ 13 | chmod -R a+w storage/ bootstrap/cache 14 | 15 | COPY ./docker/php/config/php.ini /usr/local/etc/php/php.ini 16 | COPY ./docker/php/config/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini 17 | -------------------------------------------------------------------------------- /docker/php/config/docker-php-ext-opcache.ini: -------------------------------------------------------------------------------- 1 | zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20180731/opcache.so 2 | 3 | opcache.memory_consumption=128 4 | opcache.interned_strings_buffer=8 5 | opcache.max_accelerated_files=4000 6 | opcache.revalidate_freq=60 7 | opcache.fast_shutdown=1 8 | opcache.enable_cli=1 9 | -------------------------------------------------------------------------------- /docker/php/config/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | engine = On 3 | short_open_tag = Off 4 | precision = 14 5 | output_buffering = 4096 6 | zlib.output_compression = Off 7 | implicit_flush = Off 8 | unserialize_callback_func = 9 | serialize_precision = -1 10 | disable_functions = 11 | disable_classes = 12 | zend.enable_gc = On 13 | expose_php = Off 14 | max_execution_time = 30 15 | max_input_time = 60 16 | memory_limit = 128M 17 | error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT 18 | display_errors = Off 19 | display_startup_errors = Off 20 | log_errors = On 21 | log_errors_max_len = 1024 22 | ignore_repeated_errors = Off 23 | ignore_repeated_source = Off 24 | report_memleaks = On 25 | html_errors = On 26 | error_log = /var/log/php-error.log 27 | variables_order = "GPCS" 28 | request_order = "GP" 29 | register_argc_argv = Off 30 | auto_globals_jit = On 31 | post_max_size = 8M 32 | auto_prepend_file = 33 | auto_append_file = 34 | default_mimetype = "text/html" 35 | default_charset = "UTF-8" 36 | doc_root = 37 | user_dir = 38 | enable_dl = Off 39 | file_uploads = On 40 | upload_max_filesize = 2M 41 | max_file_uploads = 20 42 | allow_url_fopen = On 43 | allow_url_include = Off 44 | default_socket_timeout = 60 45 | 46 | [CLI Server] 47 | cli_server.color = On 48 | 49 | [Date] 50 | date.timezone = Asia/Tokyo 51 | 52 | [filter] 53 | 54 | [iconv] 55 | 56 | [intl] 57 | 58 | [sqlite3] 59 | 60 | [Pcre] 61 | pcre.jit=0 62 | 63 | [Pdo] 64 | 65 | [Pdo_mysql] 66 | pdo_mysql.cache_size = 2000 67 | pdo_mysql.default_socket= 68 | 69 | [Phar] 70 | 71 | [mail function] 72 | sendmail_path = /usr/sbin/sendmail -t -i 73 | mail.add_x_header = On 74 | 75 | [ODBC] 76 | odbc.allow_persistent = On 77 | odbc.check_persistent = On 78 | odbc.max_persistent = -1 79 | odbc.max_links = -1 80 | odbc.defaultlrl = 4096 81 | odbc.defaultbinmode = 1 82 | 83 | [Interbase] 84 | ibase.allow_persistent = 1 85 | ibase.max_persistent = -1 86 | ibase.max_links = -1 87 | ibase.timestampformat = "%Y-%m-%d %H:%M:%S" 88 | ibase.dateformat = "%Y-%m-%d" 89 | ibase.timeformat = "%H:%M:%S" 90 | 91 | [MySQLi] 92 | mysqli.max_persistent = -1 93 | mysqli.allow_persistent = On 94 | mysqli.max_links = -1 95 | mysqli.cache_size = 2000 96 | mysqli.default_port = 3306 97 | mysqli.default_socket = 98 | mysqli.default_host = 99 | mysqli.default_user = 100 | mysqli.default_pw = 101 | mysqli.reconnect = Off 102 | 103 | [mysqlnd] 104 | mysqlnd.collect_statistics = On 105 | mysqlnd.collect_memory_statistics = Off 106 | 107 | [PostgreSQL] 108 | pgsql.allow_persistent = On 109 | pgsql.auto_reset_persistent = Off 110 | pgsql.max_persistent = -1 111 | pgsql.max_links = -1 112 | pgsql.ignore_notice = 0 113 | pgsql.log_notice = 0 114 | 115 | [bcmath] 116 | bcmath.scale = 0 117 | 118 | [browscap] 119 | 120 | [Session] 121 | session.save_handler = files 122 | session.use_strict_mode = 0 123 | session.use_cookies = 1 124 | session.use_only_cookies = 1 125 | session.name = PHPSESSID 126 | session.auto_start = 0 127 | session.cookie_lifetime = 0 128 | session.cookie_path = / 129 | session.cookie_domain = 130 | session.cookie_httponly = 131 | session.serialize_handler = php 132 | session.gc_probability = 1 133 | session.gc_divisor = 1000 134 | session.gc_maxlifetime = 1440 135 | session.referer_check = 136 | session.cache_limiter = nocache 137 | session.cache_expire = 180 138 | session.use_trans_sid = 0 139 | session.sid_length = 26 140 | session.trans_sid_tags = "a=href,area=href,frame=src,form=" 141 | session.sid_bits_per_character = 5 142 | 143 | [Assertion] 144 | zend.assertions = -1 145 | 146 | [mbstring] 147 | mbstring.language = Japanese 148 | mbstring.encoding_translation = Off 149 | mbstring.detect_order = UTF-8,SJIS,EUC-JP,JIS,ASCII 150 | 151 | [gd] 152 | 153 | [exif] 154 | 155 | [Tidy] 156 | tidy.clean_output = Off 157 | 158 | [soap] 159 | soap.wsdl_cache_enabled=1 160 | soap.wsdl_cache_dir="/tmp" 161 | soap.wsdl_cache_ttl=86400 162 | soap.wsdl_cache_limit = 5 163 | 164 | [sysvshm] 165 | 166 | [ldap] 167 | ldap.max_links = -1 168 | 169 | [dba] 170 | 171 | [curl] 172 | 173 | [openssl] 174 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "eslint": "eslint *.js", 5 | "format": "eslint --fix *.js", 6 | "createDotenv:local": "DEPLOY_STAGE=local node createDotenv.js", 7 | "createDotenv:dev": "DEPLOY_STAGE=dev node createDotenv.js", 8 | "createDotenv:stg": "DEPLOY_STAGE=stg node createDotenv.js", 9 | "createDotenv:prod": "DEPLOY_STAGE=prod node createDotenv.js" 10 | }, 11 | "dependencies": { 12 | "@nekonomokochan/aws-env-creator": "^2.0.0" 13 | }, 14 | "devDependencies": { 15 | "babel-eslint": "^10.0.1", 16 | "eslint": "^5.16.0", 17 | "eslint-config-prettier": "^3.6.0", 18 | "eslint-plugin-import": "^2.17.2", 19 | "eslint-plugin-prettier": "^3.0.1", 20 | "prettier": "^1.17.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Handle Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nekochans/qiita-stocker-backend/10c25b6e178577126122517fca0ee5969c0795df/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | define('LARAVEL_START', microtime(true)); 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Register The Auto Loader 15 | |-------------------------------------------------------------------------- 16 | | 17 | | Composer provides a convenient, automatically generated class loader for 18 | | our application. We just need to utilize it! We'll simply require it 19 | | into the script here so that we don't have to worry about manual 20 | | loading any of our classes later on. It feels great to relax. 21 | | 22 | */ 23 | 24 | require __DIR__.'/../vendor/autoload.php'; 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Turn On The Lights 29 | |-------------------------------------------------------------------------- 30 | | 31 | | We need to illuminate PHP development, so let us turn on the lights. 32 | | This bootstraps the framework and gets it ready for use, then it 33 | | will load up this application so that we can run it and send 34 | | the responses back to the browser and delight our users. 35 | | 36 | */ 37 | 38 | $app = require_once __DIR__.'/../bootstrap/app.php'; 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Run The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once we have the application, we can handle the incoming request 46 | | through the kernel, and send the associated response back to 47 | | the client's browser allowing them to enjoy the creative 48 | | and wonderful application we have prepared for them. 49 | | 50 | */ 51 | 52 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 53 | 54 | $response = $kernel->handle( 55 | $request = Illuminate\Http\Request::capture() 56 | ); 57 | 58 | $response->send(); 59 | 60 | $kernel->terminate($request, $response); 61 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /push-ecr-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$1" = "" ]]; then 4 | echo "AWSアカウントIDを第1引数に指定して下さい" 5 | exit 1 6 | fi 7 | 8 | if [[ "$2" = "" ]]; then 9 | echo "imageTagを第2引数に指定してください" 10 | exit 1 11 | fi 12 | 13 | awsAccountId="$1" 14 | imageTag="$2" 15 | nginxRepositoryName=stg-api-nginx 16 | phpRepositoryName=stg-api-php 17 | 18 | $(aws ecr get-login --no-include-email --region ap-northeast-1 --profile qiita-stocker-dev) 19 | 20 | docker build -t ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${nginxRepositoryName}:latest -f docker/nginx/Dockerfile . 21 | docker tag ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${nginxRepositoryName}:latest ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${nginxRepositoryName}:${imageTag} 22 | docker push ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${nginxRepositoryName}:latest 23 | docker push ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${nginxRepositoryName}:${imageTag} 24 | 25 | docker build -t ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${phpRepositoryName}:latest -f docker/php/Dockerfile . 26 | docker tag ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${phpRepositoryName}:latest ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${phpRepositoryName}:${imageTag} 27 | docker push ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${phpRepositoryName}:latest 28 | docker push ${awsAccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${phpRepositoryName}:${imageTag} 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # qiita-stocker-backend 2 | [![CircleCI](https://circleci.com/gh/nekochans/qiita-stocker-backend.svg?style=svg)](https://circleci.com/gh/nekochans/qiita-stocker-backend) 3 | 4 | ## 環境変数の設定 5 | 6 | 1. `.env`、`.env.testing`ファイルを作成します。 7 | 8 | 2. 以下の環境変数を設定します。 9 | 10 | ``` 11 | //.env 12 | DB_CONNECTION=mysql 13 | DB_HOST=127.0.0.1 14 | DB_PORT=3306 15 | DB_DATABASE=qiita_stocker 16 | DB_USERNAME=qiita_stocker 17 | DB_PASSWORD=(YourPassword999) 18 | CORS_ORIGIN=http://localhost:8080 19 | MAINTENANCE_MODE=false 20 | 21 | //.env.testing 22 | DB_CONNECTION=mysql 23 | DB_HOST=127.0.0.1 24 | DB_PORT=3306 25 | DB_DATABASE=qiita_stocker_test 26 | DB_USERNAME=qiita_stocker_test 27 | DB_PASSWORD=(YourPassword999) 28 | CORS_ORIGIN=http://localhost:8080 29 | MAINTENANCE_MODE=false 30 | ``` 31 | 32 | ## DockerfileのBuildを行いECRにプッシュする 33 | 34 | ### ローカル環境からECRにDockerイメージをプッシュする 35 | 36 | `push-ecr-local.sh` を実行して下さい。 37 | 38 | 例えば利用しているAWSアカウントが `000000000000` で Dockerイメージに `1.0.0` のタグを付けたい場合は以下のように実行します。 39 | 40 | ```bash 41 | ./push-ecr-local.sh 000000000000 1.0.0 42 | ``` 43 | 44 | ### CodeBuildプロジェクトからECRにDockerイメージをプッシュする 45 | 46 | `buildspec-push-ecr.yml` が実行されます。 47 | 48 | CodeBuildの定義自体は [こちら](https://github.com/nekochans/qiita-stocker-terraform/blob/master/modules/aws/api/codebuild.tf) に定義されています。 49 | 50 | 一時的な動作確認時には `push-ecr-local.sh` を使い、本番反映する際にはこちらのCodeBuildプロジェクトのほうを利用するのが良いでしょう。 51 | 52 | ## CircleCIをローカル上で実行する 53 | 54 | 以下の手順を実行するとCircleCIがローカル上で実行出来るようになります。 55 | 56 | CircleCI自体がDockerを使うので、Docker上ではなくあくまでもMac上で実行する必要があります。 57 | 58 | 詳しい手順はこちらを見て下さい。 59 | 60 | https://circleci.com/docs/2.0/local-cli/ 61 | 62 | 依存packageとしてDocker for Macが入ってきますが、大抵の場合、既にインストール済みだと思うので以下を実行すれば良いでしょう。 63 | 64 | ```bash 65 | brew install --ignore-dependencies circleci 66 | ``` 67 | 68 | プロジェクトルートで以下を実行するとBuildが実行されます。 69 | 70 | ```bash 71 | circleci build 72 | ``` 73 | -------------------------------------------------------------------------------- /resources/assets/js/app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * First we will load all of this project's JavaScript dependencies which 4 | * includes Vue and other libraries. It is a great starting point when 5 | * building robust, powerful web applications using Vue and Laravel. 6 | */ 7 | 8 | require('./bootstrap'); 9 | 10 | window.Vue = require('vue'); 11 | 12 | /** 13 | * Next, we will create a fresh Vue application instance and attach it to 14 | * the page. Then, you may begin adding components to this application 15 | * or customize the JavaScript scaffolding to fit your unique needs. 16 | */ 17 | 18 | Vue.component('example-component', require('./components/ExampleComponent.vue')); 19 | 20 | const app = new Vue({ 21 | el: '#app' 22 | }); 23 | -------------------------------------------------------------------------------- /resources/assets/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | 2 | window._ = require('lodash'); 3 | window.Popper = require('popper.js').default; 4 | 5 | /** 6 | * We'll load jQuery and the Bootstrap jQuery plugin which provides support 7 | * for JavaScript based Bootstrap features such as modals and tabs. This 8 | * code may be modified to fit the specific needs of your application. 9 | */ 10 | 11 | try { 12 | window.$ = window.jQuery = require('jquery'); 13 | 14 | require('bootstrap'); 15 | } catch (e) {} 16 | 17 | /** 18 | * We'll load the axios HTTP library which allows us to easily issue requests 19 | * to our Laravel back-end. This library automatically handles sending the 20 | * CSRF token as a header based on the value of the "XSRF" token cookie. 21 | */ 22 | 23 | window.axios = require('axios'); 24 | 25 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 26 | 27 | /** 28 | * Next we will register the CSRF Token as a common header with Axios so that 29 | * all outgoing HTTP requests automatically have it attached. This is just 30 | * a simple convenience so we don't have to attach every token manually. 31 | */ 32 | 33 | let token = document.head.querySelector('meta[name="csrf-token"]'); 34 | 35 | if (token) { 36 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 37 | } else { 38 | console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); 39 | } 40 | 41 | /** 42 | * Echo exposes an expressive API for subscribing to channels and listening 43 | * for events that are broadcast by Laravel. Echo and event broadcasting 44 | * allows your team to easily build robust real-time web applications. 45 | */ 46 | 47 | // import Echo from 'laravel-echo' 48 | 49 | // window.Pusher = require('pusher-js'); 50 | 51 | // window.Echo = new Echo({ 52 | // broadcaster: 'pusher', 53 | // key: process.env.MIX_PUSHER_APP_KEY, 54 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 55 | // encrypted: true 56 | // }); 57 | -------------------------------------------------------------------------------- /resources/assets/js/components/ExampleComponent.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /resources/assets/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // Body 3 | $body-bg: #f8fafc; 4 | 5 | // Typography 6 | $font-family-sans-serif: "Nunito", sans-serif; 7 | $font-size-base: 0.9rem; 8 | $line-height-base: 1.6; 9 | 10 | // Colors 11 | $blue: #3490dc; 12 | $indigo: #6574cd; 13 | $purple: #9561e2; 14 | $pink: #f66D9b; 15 | $red: #e3342f; 16 | $orange: #f6993f; 17 | $yellow: #ffed4a; 18 | $green: #38c172; 19 | $teal: #4dc0b5; 20 | $cyan: #6cb2eb; 21 | -------------------------------------------------------------------------------- /resources/assets/sass/app.scss: -------------------------------------------------------------------------------- 1 | 2 | // Fonts 3 | @import url('https://fonts.googleapis.com/css?family=Nunito'); 4 | 5 | // Variables 6 | @import 'variables'; 7 | 8 | // Bootstrap 9 | @import '~bootstrap/scss/bootstrap'; 10 | 11 | .navbar-laravel { 12 | background-color: #fff; 13 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); 14 | } 15 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least six characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have e-mailed your password reset link!', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that e-mail address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Laravel 9 | 10 | 11 | 12 | 13 | 14 | 66 | 67 | 68 |
69 | @if (Route::has('login')) 70 | 78 | @endif 79 | 80 |
81 |
82 | Laravel 83 |
84 | 85 | 92 |
93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 16 | Route::options('accounts', function () { 17 | return response()->json(); 18 | }); 19 | 20 | Route::post('accounts', 'AccountController@create'); 21 | 22 | Route::delete('accounts', 'AccountController@destroy'); 23 | 24 | Route::options('login-sessions', function () { 25 | return response()->json(); 26 | }); 27 | 28 | Route::post('login-sessions', 'LoginSessionController@create'); 29 | 30 | Route::delete('login-sessions', 'LoginSessionController@destroy'); 31 | 32 | Route::options('categories/{id?}', function () { 33 | return response()->json(); 34 | }); 35 | 36 | Route::get('categories', 'CategoryController@index'); 37 | 38 | Route::post('categories', 'CategoryController@create'); 39 | 40 | Route::patch('categories/{id}', 'CategoryController@update'); 41 | 42 | Route::delete('categories/{id}', 'CategoryController@destroy'); 43 | 44 | Route::options('stocks', function () { 45 | return response()->json(); 46 | }); 47 | 48 | Route::get('stocks', 'StockController@index'); 49 | 50 | Route::options('stocks/categories/{id?}', function () { 51 | return response()->json(); 52 | }); 53 | 54 | Route::get('stocks/categories/{id}', 'StockController@showCategorized'); 55 | 56 | Route::options('categories/stocks/{id?}', function () { 57 | return response()->json(); 58 | }); 59 | 60 | Route::post('categories/stocks', 'CategoryController@categorize'); 61 | 62 | Route::delete('categories/stocks/{id}', 'CategoryController@destroyRelation'); 63 | 64 | Route::options('statuses', function () { 65 | return response()->json(); 66 | }); 67 | Route::get('statuses', 'StatusController@index'); 68 | }); 69 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 16 | }); 17 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 18 | })->describe('Display an inspiring quote'); 19 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/AbstractTestCase.php: -------------------------------------------------------------------------------- 1 | beforeApplicationDestroyed(function () { 50 | \DB::disconnect(); 51 | }); 52 | 53 | parent::tearDown(); 54 | } 55 | 56 | /** 57 | * テスト用のモックを設定する 58 | * 59 | * @param $responses 60 | */ 61 | protected function setMockGuzzle($responses) 62 | { 63 | app()->bind(Client::class, function () use ($responses) { 64 | $mock = []; 65 | foreach ($responses as $response) { 66 | $mock[] = new Response($response[0], $response[1] ?? [], $response[2] ?? null); 67 | } 68 | $mock = new MockHandler($mock); 69 | $handler = HandlerStack::create($mock); 70 | 71 | return new Client(['handler' => $handler]); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 17 | 18 | $response->assertStatus(200); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Feature/StatusIndexTest.php: -------------------------------------------------------------------------------- 1 | get( 21 | '/api/statuses' 22 | ); 23 | 24 | // 実際にJSONResponseに期待したデータが含まれているか確認する 25 | $jsonResponse->assertStatus(200); 26 | $jsonResponse->assertHeader('X-Request-Id'); 27 | } 28 | 29 | /** 30 | * 正常系のテスト 31 | * メンテナンス中もエラーとならないこと 32 | */ 33 | public function testSuccessMaintenance() 34 | { 35 | \Config::set('app.maintenance', true); 36 | $jsonResponse = $this->get( 37 | '/api/statuses' 38 | ); 39 | 40 | // 実際にJSONResponseに期待したデータが含まれているか確認する 41 | $jsonResponse->assertStatus(200); 42 | $jsonResponse->assertHeader('X-Request-Id'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | --------------------------------------------------------------------------------