Showing preview only (3,191K chars total). Download the full file or copy to clipboard to get everything.
Repository: appkr/l5essential
Branch: master
Commit: 428a0051c85a
Files: 287
Total size: 2.8 MB
Directory structure:
gitextract_5wn34sgq/
├── .bowerrc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── Envoy.blade.php
├── LICENSE
├── Vagrantfile
├── apiary.apib
├── app/
│ ├── Article.php
│ ├── Attachment.php
│ ├── AuthorTrait.php
│ ├── Comment.php
│ ├── Console/
│ │ ├── Commands/
│ │ │ ├── BackupDb.php
│ │ │ ├── ClearLog.php
│ │ │ ├── Inspire.php
│ │ │ ├── PruneRelease.php
│ │ │ └── UpdateLessonsTable.php
│ │ └── Kernel.php
│ ├── Events/
│ │ ├── ArticleConsumed.php
│ │ ├── Event.php
│ │ └── ModelChanged.php
│ ├── Exceptions/
│ │ └── Handler.php
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Api/
│ │ │ │ ├── PasswordsController.php
│ │ │ │ ├── SessionsController.php
│ │ │ │ ├── UsersController.php
│ │ │ │ ├── V1/
│ │ │ │ │ ├── ArticlesController.php
│ │ │ │ │ ├── CommentsController.php
│ │ │ │ │ └── WelcomeController.php
│ │ │ │ └── WelcomeController.php
│ │ │ ├── ArticlesController.php
│ │ │ ├── AttachmentsController.php
│ │ │ ├── Cacheable.php
│ │ │ ├── CommentsController.php
│ │ │ ├── Controller.php
│ │ │ ├── LessonsController.php
│ │ │ ├── PasswordsController.php
│ │ │ ├── SessionsController.php
│ │ │ ├── SocialController.php
│ │ │ ├── UsersController.php
│ │ │ └── WelcomeController.php
│ │ ├── Kernel.php
│ │ ├── Middleware/
│ │ │ ├── Authenticate.php
│ │ │ ├── AuthorOnly.php
│ │ │ ├── EncryptCookies.php
│ │ │ ├── GetUserFromToken.php
│ │ │ ├── ObfuscateId.php
│ │ │ ├── RedirectIfAuthenticated.php
│ │ │ ├── RefreshToken.php
│ │ │ ├── ThrottleApiRequests.php
│ │ │ └── VerifyCsrfToken.php
│ │ ├── Requests/
│ │ │ ├── ArticlesRequest.php
│ │ │ ├── FilterArticlesRequest.php
│ │ │ └── Request.php
│ │ └── routes.php
│ ├── Jobs/
│ │ └── Job.php
│ ├── Lesson.php
│ ├── Listeners/
│ │ ├── .gitkeep
│ │ ├── CacheHandler.php
│ │ ├── CommentsHandler.php
│ │ ├── UserEventsHandler.php
│ │ └── ViewCountHandler.php
│ ├── Model.php
│ ├── Policies/
│ │ └── .gitkeep
│ ├── Providers/
│ │ ├── AppServiceProvider.php
│ │ ├── AuthServiceProvider.php
│ │ ├── EventServiceProvider.php
│ │ └── RouteServiceProvider.php
│ ├── Reporters/
│ │ ├── ErrorReport.php
│ │ └── MonologSlackReport.php
│ ├── Repositories/
│ │ ├── LessonRepository.php
│ │ ├── MarkdownRepository.php
│ │ └── RepositoryInterface.php
│ ├── Services/
│ │ └── Markdown.php
│ ├── Tag.php
│ ├── Transformers/
│ │ ├── ArticleTransformer.php
│ │ ├── AttachmentTransformer.php
│ │ ├── CommentTransformer.php
│ │ ├── TagTransformer.php
│ │ └── UserTransformer.php
│ ├── User.php
│ ├── Vote.php
│ └── helpers.php
├── artisan
├── bootstrap/
│ ├── app.php
│ ├── autoload.php
│ └── cache/
│ └── .gitignore
├── bower.json
├── composer.json
├── config/
│ ├── api.php
│ ├── app.php
│ ├── auth.php
│ ├── broadcasting.php
│ ├── cache.php
│ ├── compile.php
│ ├── cors.php
│ ├── database.php
│ ├── filesystems.php
│ ├── icons.php
│ ├── jwt.php
│ ├── mail.php
│ ├── project.php
│ ├── queue.php
│ ├── roles.php
│ ├── services.php
│ ├── session.php
│ ├── slack.php
│ └── view.php
├── database/
│ ├── .gitignore
│ ├── factories/
│ │ └── ModelFactory.php
│ ├── migrations/
│ │ ├── .gitkeep
│ │ ├── 2014_10_12_000000_create_users_table.php
│ │ ├── 2014_10_12_100000_create_password_resets_table.php
│ │ ├── 2015_01_15_105324_create_roles_table.php
│ │ ├── 2015_01_15_114412_create_role_user_table.php
│ │ ├── 2015_01_26_115212_create_permissions_table.php
│ │ ├── 2015_01_26_115523_create_permission_role_table.php
│ │ ├── 2015_02_09_132439_create_permission_user_table.php
│ │ ├── 2015_11_20_062500_create_comments_table.php
│ │ ├── 2015_11_20_062513_create_articles_table.php
│ │ ├── 2015_11_20_062601_create_tags_table.php
│ │ ├── 2015_11_20_062613_create_attachments_table.php
│ │ ├── 2015_11_20_062846_create_article_tag_table.php
│ │ ├── 2015_12_09_165930_create_lessons_table.php
│ │ └── 2015_12_10_151357_create_votes_table.php
│ └── seeds/
│ ├── .gitkeep
│ └── DatabaseSeeder.php
├── gulpfile.js
├── lessons/
│ ├── 01-welcome.md
│ ├── 02-hello-laravel.md
│ ├── 02-install-homestead-osx.md
│ ├── 02-install-homestead-windows.md
│ ├── 02-install-on-windows.md
│ ├── 03-configuration.md
│ ├── 04-routing-basics.md
│ ├── 05-pass-data-to-view.md
│ ├── 06-blade-101.md
│ ├── 07-blade-201.md
│ ├── 08-raw-queries.md
│ ├── 09-query-builder.md
│ ├── 10-eloquent.md
│ ├── 11-migration.md
│ ├── 12-controller.md
│ ├── 13-restful-resource-controller.md
│ ├── 14-named-routes.md
│ ├── 15-nested-resources.md
│ ├── 16-authentication.md
│ ├── 17-authentication-201.md
│ ├── 18-eloquent-relationships.md
│ ├── 19-seeder.md
│ ├── 20-1-pagination.md
│ ├── 20-eager-loading.md
│ ├── 21-mail.md
│ ├── 22-events.md
│ ├── 23-validation.md
│ ├── 24-exception-handling.md
│ ├── 25-composer.md
│ ├── 26-document-model.md
│ ├── 27-document-controller.md
│ ├── 28-cache.md
│ ├── 29-elixir.md
│ ├── 30-final-touch.md
│ ├── 31-forum-features.md
│ ├── 32-login.md
│ ├── 32n33-auth-refactoring.md
│ ├── 33-social-login.md
│ ├── 34-role.md
│ ├── 35-locale.md
│ ├── 36-models.md
│ ├── 37-articles.md
│ ├── 38-tags.md
│ ├── 39-attachments.md
│ ├── 40-comments.md
│ ├── 41-ui-makeup.md
│ ├── 42-be-makeup.md
│ ├── 43-change-note.md
│ ├── 44-api-basic.md
│ ├── 45-api-big-picture.md
│ ├── 46-jwt.md
│ ├── 47-dry-refactoring.md
│ ├── 48-all-is-bad.md
│ ├── 49-rate-limit.md
│ ├── 50-id-obfuscation.md
│ ├── 51-cors.md
│ ├── 52-caching.md
│ ├── 53-partial-response.md
│ ├── 54-api-docs.md
│ ├── 999-code-release.md
│ ├── INDEX.md
│ └── images/
│ ├── 02-hello-laravel-img-01.pptx
│ ├── 02-hello-laravel-img-03.pptx
│ ├── 32n33-auth-refactoring-img-02.pptx
│ └── 46-jwt-img-01.pptx
├── package.json
├── phpunit.xml
├── public/
│ ├── .htaccess
│ ├── attachments/
│ │ └── .gitignore
│ ├── build/
│ │ ├── css/
│ │ │ └── app-4cd4d601dd.css
│ │ ├── fonts/
│ │ │ └── FontAwesome.otf
│ │ ├── js/
│ │ │ ├── app-038b8ad709.js
│ │ │ └── app-6c3ef62a70.js
│ │ └── rev-manifest.json
│ ├── index.php
│ └── robots.txt
├── readme.md
├── resources/
│ ├── assets/
│ │ ├── js/
│ │ │ └── app.js
│ │ └── sass/
│ │ ├── _auth.scss
│ │ ├── _commons.scss
│ │ ├── _forum.scss
│ │ ├── _landing.scss
│ │ ├── _lessons.scss
│ │ ├── _mediaquery.scss
│ │ └── app.scss
│ ├── lang/
│ │ ├── en/
│ │ │ ├── auth.php
│ │ │ ├── common.php
│ │ │ ├── errors.php
│ │ │ ├── forum.php
│ │ │ ├── lessons.php
│ │ │ ├── pagination.php
│ │ │ ├── passwords.php
│ │ │ └── validation.php
│ │ └── ko/
│ │ ├── auth.php
│ │ ├── common.php
│ │ ├── errors.php
│ │ ├── forum.php
│ │ ├── lessons.php
│ │ ├── pagination.php
│ │ ├── passwords.php
│ │ └── validation.php
│ └── views/
│ ├── articles/
│ │ ├── create.blade.php
│ │ ├── edit.blade.php
│ │ ├── index.blade.php
│ │ ├── partial/
│ │ │ ├── article.blade.php
│ │ │ ├── form.blade.php
│ │ │ └── search.blade.php
│ │ └── show.blade.php
│ ├── attachments/
│ │ └── partial/
│ │ └── list.blade.php
│ ├── comments/
│ │ ├── index.blade.php
│ │ └── partial/
│ │ ├── best.blade.php
│ │ ├── comment.blade.php
│ │ ├── control.blade.php
│ │ ├── create.blade.php
│ │ ├── edit.blade.php
│ │ └── login.blade.php
│ ├── emails/
│ │ ├── new-comment.blade.php
│ │ └── password.blade.php
│ ├── errors/
│ │ ├── 503.blade.php
│ │ └── notice.blade.php
│ ├── home.blade.php
│ ├── layouts/
│ │ ├── master.blade.php
│ │ └── partial/
│ │ ├── flash_message.blade.php
│ │ ├── flash_modal.blade.php
│ │ ├── footer.blade.php
│ │ ├── markdown.blade.php
│ │ ├── navigation.blade.php
│ │ └── tracker.blade.php
│ ├── lessons/
│ │ ├── partial/
│ │ │ └── pager.blade.php
│ │ └── show.blade.php
│ ├── passwords/
│ │ ├── remind.blade.php
│ │ └── reset.blade.php
│ ├── sessions/
│ │ └── create.blade.php
│ ├── tags/
│ │ └── partial/
│ │ ├── index.blade.php
│ │ └── list.blade.php
│ ├── users/
│ │ ├── create.blade.php
│ │ └── partial/
│ │ └── avatar.blade.php
│ └── vendor/
│ ├── .gitkeep
│ └── flash/
│ ├── message.blade.php
│ └── modal.blade.php
├── server.php
├── storage/
│ ├── app/
│ │ └── .gitignore
│ ├── backup/
│ │ └── .gitignore
│ ├── framework/
│ │ ├── .gitignore
│ │ ├── cache/
│ │ │ └── .gitignore
│ │ ├── sessions/
│ │ │ └── .gitignore
│ │ └── views/
│ │ └── .gitignore
│ └── logs/
│ └── .gitignore
└── tests/
├── TestCase.php
└── integration/
└── Http/
└── Controllers/
├── Api/
│ ├── ApiTest.php
│ ├── PasswordsController.php
│ ├── SessionsController.php
│ ├── UsersController.php
│ └── V1/
│ └── ArticlesController.php
├── AuthTest.php
├── SessionsController.php
├── UsersController.php
└── WelcomeController.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .bowerrc
================================================
{"directory":"resources/assets/vendor","analytics":false}
================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.php]
indent_size = 4
[**.blade.php]
indent_size = 2
================================================
FILE: .gitattributes
================================================
* text=auto
*.css linguist-vendored
*.less linguist-vendored
================================================
FILE: .gitignore
================================================
.env
.idea
.DS_Store
.phpstorm*
_ide_helper*
dredd.*
git_*
Homestead.*
node_modules
npm-debug.log
/public/css
/public/js
/resources/assets/css
/resources/assets/vendor
/tests/database.sqlite
/vendor
================================================
FILE: .travis.yml
================================================
language: php
php:
- 5.6
- 7.0
sudo: false
install:
- composer self-update
- travis_retry composer install --no-interaction --no-scripts --prefer-source --dev
before_script:
- cp .env.example .env
- touch tests/database.sqlite
- php artisan key:generate
- php artisan clear-compiled
- php artisan optimize
- php artisan cache:clear
- php artisan migrate --env="testing" --database="sqlite" --force
- php artisan db:seed --env="testing" --database="sqlite" --force
script: vendor/bin/phpunit
matrix:
fast_finish: true
================================================
FILE: CONTRIBUTING.md
================================================
# Contribution Guide
이 강좌에 여러가지 방법으로 기여할 수 있습니다.
- 강좌 오류 수정
- 코드 오류 수정
- ...
기여자 분들은 기여의 형태에 따라 [Contributors / Sponsors](https://github.com/appkr/l5essential#contributors--sponsors) 또는 [Contributor List](https://github.com/appkr/l5essential/graphs/contributors) 에 등록됩니다. 이 강좌는 독자 및 기여자 여러분들과 같이 만들어 가는 것입니다. 여러 분들의 기여 활동은 이 강좌를 접하는 많은 분들에게 도움이 될 것입니다.
## Pull Request
- 코드는 [PSR-2 코딩 컨벤션](http://www.php-fig.org/psr/psr-2/) 을 지켜 주세요.
- 수정 내용을 설명해 주세요.
- Fork 한후 Topic Branch 를 만들어서 PR 을 보내 주세요.
- PR 전에 최신 코드인 지를 확인해 주세요. (`$ git pull && git merge master`)
- 단순 오류 수정이 아니라, 동작을 변경하는 코드라면 Test 를 포함해 주세요.
## Test
이 강좌의 코드는 Integration Test 를 포함하고 있습니다. 코드를 수정했다면 PR 전에 테스트를 실행해 주세요. PR 을 하시면 Travis CI 가 테스트를 한번 더 수행합니다.
```sh
$ phpunit
```
감사합니다.
appkr.
================================================
FILE: Envoy.blade.php
================================================
#--------------------------------------------------------------------------
# List of tasks, that you can run...
# e.g. envoy run hello
#--------------------------------------------------------------------------
#
# hello Check ssh connection
# release Publish new release
# list Show list of releases
# checkout Checkout to the given release (must provide --release=/path/to/release)
# prune Purge old releases (must provide --keep=n, where n is a number)
#
@servers(['web' => 'aws-seoul-deploy'])
@setup
$path = [
'base' => '/home/deployer/www',
'docroot' => '/home/deployer/www/l5.appkr.kr',
'shared' => '/home/deployer/www/shared',
'release' => '/home/deployer/www/releases',
];
$required_dirs = [
$path['base'],
$path['shared'],
$path['release'],
];
$shared_item = [
'/home/deployer/www/shared/.env' => '.env',
'/home/deployer/www/shared/storage' => 'storage',
'/home/deployer/www/shared/cache' => 'bootstrap/cache',
'/home/deployer/www/shared/attachments' => 'public/attachments',
];
$distribution = [
'name' => 'release_' . date('YmdHis'),
];
$git = [
'repo' => 'git@github.com:appkr/l5essential.git',
];
@endsetup
@task('hello', ['on' => ['web']])
HOSTNAME=$(hostname);
echo "Hello Envoy! Responding from $HOSTNAME";
@endtask
@task('release', ['on' => ['web']])
{{--Create directories if not exists--}}
@foreach ($required_dirs as $dir)
if [ ! -d {{ $dir }} ]; then
mkdir {{ $dir }};
chgrp -h -R www-data {{ $dir }};
fi;
@endforeach
{{--Download book keeping officer--}}
if [ ! -f {{ $path['base'] }}/officer.php ]; then
wget https://raw.githubusercontent.com/appkr/envoy/master/scripts/officer.php -O {{ $path['base'] }}/officer.php;
fi;
{{--Fetch code from git--}}
cd {{ $path['release'] }};
git clone -b master {{ $git['repo'] }} {{ $distribution['name'] }};
{{--Symlink shared directory to current release.--}}
{{--e.g. storage, .env, user uploaded file storage, ...--}}
cd {{ $path['release'] }}/{{ $distribution['name'] }};
@foreach($shared_item as $global => $local)
[ -f {{ $local }} ] && rm {{ $local }};
[ -d {{ $local }} ] && rm -rf {{ $local }};
ln -nfs {{ $global }} {{ $local }};
chgrp -h -R www-data {{ $local }};
@endforeach
{{--Run composer install--}}
composer install --prefer-dist --no-scripts;
php artisan clear-compiled;
php artisan optimize;
php artisan cache:clear;
php artisan my:update-lesson;
{{--Symlink current release to service directory.--}}
ln -nfs {{ $path['release'] }}/{{ $distribution['name'] }} {{ $path['docroot'] }};
chgrp -h -R www-data {{ $path['docroot'] }};
{{--Set permission and change owner. Do one final more for safety.--}}
chgrp -h -R www-data {{ $path['release'] }}/{{ $distribution['name'] }};
{{--Book keeping--}}
php {{ $path['base'] }}/officer.php deploy {{ $path['release'] }}/{{ $distribution['name'] }};
{{--Restart web server.--}}
sudo service nginx restart;
sudo service php5-fpm restart;
@endtask
@task('prune', ['on' => 'web'])
if [ ! -f {{ $path['base'] }}/officer.php ]; then
echo '"officer.php" script not found.';
echo '\$ envoy run hire_officer';
exit 1;
fi;
@if (isset($keep) and $keep > 0)
php {{ $path['base'] }}/officer.php prune {{ $keep }};
@else
echo 'Must provide --keep=n, where n is a number.';
@endif
@endtask
@task('hire_officer', ['on' => 'web'])
{{--Download "officer.php" to the server--}}
wget https://raw.githubusercontent.com/appkr/envoy/master/scripts/officer.php -O {{ $path['base'] }}/officer.php;
echo '"officer.php" is ready!';
@endtask
@task('list', ['on' => 'web'])
{{--Show the list of release--}}
if [ ! -f {{ $path['base'] }}/officer.php ]; then
echo '"officer.php" script not found.';
echo '\$ envoy run hire_officer';
exit 1;
fi;
php {{ $path['base'] }}/officer.php list;
@endtask
@task('checkout', ['on' => 'web'])
{{--Checkout to the given release path--}}
if [ ! -f {{ $path['base'] }}/officer.php ]; then
echo '"officer.php" script not found.';
echo '\$ envoy run hire_officer';
exit 1;
fi;
@if (isset($release))
cd {{ $release }};
{{--Symlink shared directory to the given release.--}}
@foreach($shared_item as $global => $local)
[ -f {{ $local }} ] && rm {{ $local }};
[ -d {{ $local }} ] && rm -rf {{ $local }};
ln -nfs {{ $global }} {{ $local }};
chgrp -h -R www-data {{ $local }};
@endforeach
{{--Symlink the given release to service directory.--}}
ln -nfs {{ $release }} {{ $path['docroot'] }};
chgrp -h -R www-data {{ $path['docroot'] }};
{{--Book keeping--}}
php {{ $path['base'] }}/officer.php checkout {{ $release }};
{{--Restart web server.--}}
sudo service nginx restart;
sudo service php5-fpm restart;
@else
echo 'Must provide --release=/full/path/to/release.';
@endif
@endtask
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Appkr <juwonkim@me.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Vagrantfile
================================================
require 'json'
require 'yaml'
VAGRANTFILE_API_VERSION ||= "2"
confDir = $confDir ||= File.expand_path("vendor/laravel/homestead", File.dirname(__FILE__))
homesteadYamlPath = "Homestead.yaml"
homesteadJsonPath = "Homestead.json"
afterScriptPath = "after.sh"
aliasesPath = "aliases"
require File.expand_path(confDir + '/scripts/homestead.rb')
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
if File.exists? aliasesPath then
config.vm.provision "file", source: aliasesPath, destination: "~/.bash_aliases"
end
if File.exists? homesteadYamlPath then
Homestead.configure(config, YAML::load(File.read(homesteadYamlPath)))
elsif File.exists? homesteadJsonPath then
Homestead.configure(config, JSON.parse(File.read(homesteadJsonPath)))
end
if File.exists? afterScriptPath then
config.vm.provision "shell", path: afterScriptPath
end
end
================================================
FILE: apiary.apib
================================================
FORMAT: 1A
HOST: http://api.appkr.kr
# Welcome to the myProject Api
myProject Api 에 오신 것을 환영합니다.
myProject 에서는 포럼 <sup>Forum</sup> Api 를 제공합니다. 포럼을 사용하는 사용자를 식별하고 권한을 제어하기 위해 사용자 등록 및 로그인 <sup>Authentication</sup> 기능도 제공합니다.
## Base URL
모든 Api 요청은 아래 Base URL 을 이용합니다.
```http
http://api.appkr.kr
```
## Current Version
현재 버전은 v1 입니다. [Authentication](#authentication) 을 제외하고 모든 Api 요청에 `/v1` 이 포함되어야 합니다.
```http
GET /v1 HTTP/1.1
Host: api.appkr.kr
```
## Request 101
REST 원칙을 따릅니다.
### Reqeust Headers
#### Accept (Content Negotiation)
v1 에서는 JSON 응답만 지원합니다.
```http
GET /v1 HTTP/1.1
Host: api.appkr.kr
Accept: application/json
```
#### Accept-Language (Language Negoation)
v1 에서는 ko_KR 만 지원합니다.
```http
GET /v1 HTTP/1.1
Host: api.appkr.kr
Accept-Language: ko-KR
```
### Request Payload
`multipart/form-data`, `application/x-www-form-urlencoded`, `application/json` 을 모두 지원합니다. `application/json` 사용을 권장합니다.
```http
POST /v1/articles HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
{"field": "value"}
```
## Response 101
### Collection Response
Collection 에 해당하는 리소스를 요청하면 Pagination 이 포함된 응답을 받습니다. `data` 키 아래에 포함된 내용이 Collection 입니다.
```http
GET /v1/articles HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
```
```json
HTTP/1.1 200 OK
{
"data": [
{"key": "value"},
{"key": "value"},
{"key": "value"},
{"key": "value"},
{"key": "value"}
],
"meta": {
"pagination": {
"total": 20,
"count": 5,
"per_page": 5,
"current_page": 1,
"total_pages": 4,
"links": {
"next": "/v1/articles?page=2"
}
}
}
}
```
### Instance Response
Instance 에 해당하는 리소스를 요청하면 요청한 Instance 하나에 대한 응답을 받습니다. 이 응답은 JSON 키가 없습니다.
```http
GET /v1/articles/95280986 HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
```
```json
HTTP/1.1 200 OK
{
"id": 95280986,
"title": "example title"
}
```
### Successful Response
리소스 생성, 수정, 삭제 등의 요청을 성공하면 성공 응답을 받습니다.
```http
PUT /v1/articles/95280986 HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
Authorization: Bearer header.payload.signagure
{"title": "modified title"}
```
```javascript
HTTP/1.1 200 OK
{
"success": {
"code": 200,
"message": "Updated"
}
}
```
성공 응답 목록은 아래와 같고, HTTP 응답 코드와 동일한 코드를 JSON 본문에도 포함합니다.
Response Code|Description
---|---
200|성공
201|새로운 리소스가 성공적으로 생성되었습니다.
204|요청한 리소스가 삭제되었습니다.
### Error Response
에러가 발생했을 때는 아래와 같은 응답을 받습니다.
```http
PUT /v1/articles/95280986 HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
Authorization: Bearer header.payload.signagure
{"title": "modified title"}
```
```javascript
HTTP/1.1 403 Forbidden
{
"error": {
"code": 403,
"message": "Forbidden"
}
}
```
오류 응답 목록은 아래와 같고, HTTP 응답 코드와 동일한 코드를 JSON 본문에도 포함합니다.
Response Code|Description
---|---
400|요청에 오류가 있습니다.
401|인증 오류입니다. Authorization 헤더를 확인해 주세요.
403|권한이 없습니다. Authorization 헤더로 넘긴 토큰에 해당하는 사용자가 요청하신 Action 을 수행할 권한이 없습니다.
404|요청한 리소스가 없습니다.
405|잘못된 HTTP 메소드를 사용했습니다.
422|유효성 검사 오류입니다. 요청 본문에 데이터가 형식에 맞는지 확인하세요.
429|Rate Limit 를 초과했습니다. 잠시 후 다시 시도하세요.
500|서버에 오류가 있습니다. [관리자](mailto:junwonkim@me.com) 에게 신고해 주세요.
503|서버에 트래픽이 폭주했거나, 서버 유지보수 작업 중입니다.
## Rate Limit
[Authentication](#authentication) 요청은 1 분에 10 회 까지, 리소스 요청은 1분에 60 회까지 허용합니다.
```http
GET /v1/articles/95280986 HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
```
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
```
## Caching
GET 동사를 이용한 요청은 `Etag` 헤더를 응답합니다.
> 대부분의 HTTP 클라이언트 라이브러리가 자동으로 모든 처리를 해 줍니다.<br/><br/>
> 다만, 여러분의 클라이언트가 `Etag` 기능을 지원하지 않는데, 캐싱의 이득을 보려면, 응답 받은 Etag 헤더를 파싱하여 요청 URL 과 응답 본문을 저장하고, 다음 요청시 `If-None-Match: Etag` 헤더를 전송해야 합니다.
아래 그림은 `Etag` 를 이용한 클라이언트-서버간 캐싱 동작 시퀀스 다이어그램입니다.

### First Request & Response
처음으로 `Etag` 값을 받습니다. 클라이언트 저장소에 받은 `Etag` 값을 키로 요청 URL 과 응답 본문을 저장해 놓습니다. *(대부분의 클라이언트 라이브러리가 자동으로 처리해 줍니다.)*
```http
GET /v1/articles/95280986 HTTP/1.1
Host: api.appkr.kr
```
```http
HTTP/1.1 200 OK
Etag: CtXS0QQlWJKY2dXe
```
### Second-on Request & Response - **No change in server resources**
요청 URL 에 해당하는 `Etag` 값을 `If-None-Match` 헤더에 달아서 보냅니다.
응답을 보니 리소스의 내용이 변경되지 않았으므로, 클라이언트 저장소에 저장된 지난 번 응답 본문을 그대로 사용합니다. *(대부분의 클라이언트 라이브러리가 자동으로 처리해 줍니다.)*
```http
GET /v1/articles/95280986 HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
If-None-Match: CtXS0QQlWJKY2dXe
```
```http
HTTP/1.1 304 Not Modified
```
### Second-on Request & Response - **Server resource changed**
서버의 리소스가 변경되었습니다. 새로 받은 `Etag` 값과 응답 본문을 클라이언트 저장소에 저장합니다. *(대부분의 클라이언트 라이브러리가 자동으로 처리해 줍니다.)*
```http
GET /v1/articles/95280986 HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
If-Non-Match: CtXS0QQlWJKY2dXe
```
```http
HTTP/1.1 200 OK
Etag: F8tG872adeIC3zlf
```
**`WHY`** 모바일 환경에서 응답속도 개선 및 사용자 요금 절감을 위해 꼭 필요합니다.
## Sub Resource Inclusion
### Signature
`include` 쿼리스트링을 이용하면, 현재 요청하는 리소스의 자식 리소스의 Collection 또는 Instance 를 응답에 포함할 수 있습니다.
문법은 아래와 같습니다.
```http
GET /v1/parent?include=child_resource:limit(limit|offset):sort(sort|order) HTTP/1.1
# 여러 개의 자식 리소스를 포함할 때는 array 필드나 콤마 (,) 를 이용할 수 있습니다.
GET /v1/parent?include[]=child_resource_1&include[]=child_resource_2 HTTP/1.1
# --or--
GET /v1/parent?include[]=child_resource_1,child_resource_2 HTTP/1.1
```
Keyword|Description
---|---
`child_resource`|자식 리소스의 이름입니다. (Article 부모 리소스에 대해서는 `comments`, `author`, `tags`, `attachments` 자식 리소스가 가용합니다.)
`:`|파라미터를 구분하는 구분자입니다.
`limit`|예약어 입니다. `comments`, `author`, `tags`, `attachments` 등 Collection 형태의 리소스에만 적용 가능합니다.
`limit(offset\\limit)`|`limit` 반환 받을 Collection 개수 입니다. `offset` 건너뛸 Collection 개수입니다. **(기본값 `limit(3\\0)`)**
`sort`|예약어 입니다. Collection 형태의 리소스에만 적용 가능합니다.
`sort(sort\\order)`|`sort` 정렬 기준 필드로 현재는 `created`, `view_count` 두 개만 지원합니다. `order` 정렬 방향으로 `asc`, `desc` 를 쓸 수 있습니다. **(기본값 `sort(created\\desc)`)**
**`알림`** 위 표에서 `\\` 는 Apiary 의 마크다운 컴파일 에러를 방지하기 위해 `|` 대신 넣은 문자입니다.
### Example
아래는 Article 리소스의 자식 리소스인 comments 와 authors 를 포함하는 예제 입니다.
```http
GET /v1/articles/95280986?include=comments:limit(1|0):sort('created'|'asc') HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
```
```javascript
HTTP/1.1 200 OK
{
"id": 95280986,
"title": "example title",
"content_raw": "example content",
"content_html": "<p>example content</p>",
"created": "2015-12-19T10:37:42+0000",
"view_count": 1,
"link": {
"rel": "self",
"href": "/v1/articles/95280986"
},
"comments": {
"data": [
{
"id": 95280986,
"content_raw": "example comment",
"content_html": "<p>example comment</p>",
"created": "2015-12-19T10:37:42+0000",
"vote": {
"up": 1,
"down": 1
},
"link": {
"rel": "self",
"href": "/v1/comments/95280986"
},
"author": {
"name": "John Doe",
"email": "john@example.com",
"avatar": "http://www.gravatar.com/d4c74594d841139328695756648b6bd6"
}
}
]
},
"author": {
"name": "John Doe",
"email": "john@example.com",
"avatar": "http://www.gravatar.com/d4c74594d841139328695756648b6bd6"
},
"tags": [
"laravel",
"eloquent"
],
"attachments": 1
}
```
## Partial Response
### Against Parent Resource
`fields` 쿼리스트링을 이용하면, 응답 필드를 골라서 받을 수 있습니다. 필드는 콤마 (`,`) 로 구분하고, 필드간에 공백이 없도록 주의하십시오.
```http
GET /v1/articles/95280986?fields=id,title,content_raw,author HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
```
```javascript
HTTP/1.1 200 OK
{
"id": 95280986,
"title": "example title",
"content_raw": "example content",
"author": {
"name": "John Doe",
"email": "john@example.com",
"avatar": "http://www.gravatar.com/d4c74594d841139328695756648b6bd6"
}
}
```
### Against Sub Resource
역시 `fields` 쿼리스트링을 이용해서 자식 리소스의 응답 필드를 골라 받을 수 있습니다. 부모 리소스에서와 달리, 필드 구분에 파이프 문자 (`|`) 를 이용합니다. 필드 사이에 공백이 없도록 주의하십시오.
```http
GET /v1/articles/95280986?fields=id,title,content_raw,author&include=comments:fields(id|created|vote) HTTP/1.1
Host: api.appkr.kr
Content-type: application/json
```
```javascript
HTTP/1.1 200 OK
{
"id": 95280986,
"title": "example title",
"content_raw": "example content",
"author": {
"name": "John Doe",
"email": "john@example.com",
"avatar": "http://www.gravatar.com/d4c74594d841139328695756648b6bd6"
},
"comments": {
"data": [
{
"id": 95280986,
"created": "2015-12-19T10:37:42+0000",
"vote": {
"up": 1,
"down": 1
}
}
]
},
}
```
# group Welcome to myProject Api v1
myProject Api v1 에 오신 것을 환영합니다.
## Welcoming the Api v1 [/v1]
### Hello myProject Api v1 [GET]
- request
- headers
Accept: application/json
- response 200 (application/json)
{
"name": "myProject Api",
"message": "Welcome to myProject Api. This is a base endpoint of version 1.",
"version": "v1",
"links": [
{
"rel": "self",
"href": "/v1"
},
{
"rel": "api.users.store",
"href": "/auth/register"
},
{
"rel": "api.sessions.store",
"href": "/auth/login"
},
{
"rel": "api.v1.docs",
"href": "/v1/docs"
}
]
}
# group Authentication
<a name="authentication"></a>
사용자 등록 및 인증을 위한 Api 입니다. myProject Api 는 사용자 인증을 위해 JWT <sup>Json Web Token</sup> 을 이용합니다.
## `token` 의 발급 및 사용
사용자 등록 및 로그인을 통해 생성된 `token` 은 myProject 서버에 새로운 리소스를 생성, 기존 리소스 수정 또는 삭제를 위한 Api 요청을 할 때 꼭 필요합니다. 전술한 Api 요청을 할 때 `token` 을 HTTP Authorization Header 에 붙여서 사용합니다. (e.g. `Authorization: Bearer token`) `token` 이 없으면 401 Unauthorized 응답을 받습니다. 한번 발급된 `token` 은 120 분 동안 유효하며, 유효한 기간 동안은 로그인 없이 Api 요청을 할 수 있습니다. 그래서, Api 클라이언트는 사용자 등록 및 로그인에서 받은 토큰을 로컬 저장소에 저장하고 있어야 합니다.
## `token` 의 만료 및 갱신
`token` 이 만료되면 서버는 401 Unauthorized 응답 코드와 함께 `token_expired` 메시지를 응답합니다. `token` 을 사용하지 않은지 총 2 주일이 지나지 않았다면, 기존 `token` 을 이용하여 새로운 토큰을 받을 수 있습니다. 2 주가 지나 더 이상 갱신이 불가한 상태가 되면, 서버로 부터 401 응답을 받게 되며 이때는 사용자에게 로그인 UI 를 표출하여 로그인을 하도록 유도하여 `token` 을 발급 받아야 합니다.
## User Registration [/auth/register]
새로운 사용자를 등록합니다.
### User Registration [POST]
- request OK
- headers
Accept: application/json
Content-type: application/json
- body
{
"name": "New User",
"email": "new_user@example.org",
"password": "password",
"password_confirmation": "password"
}
- response 201 (application/json)
- Attributes
- success (object)
- code: `201` (number) - Http Status Equivalent Message Code
- message: `Created` (string) - Message
- meta (object)
- token: `header.payload.signature` (string) - Json Web Token
- request Invalid Data
- headers
Accept: application/json
Content-type: application/json
- body
{"email": "invalid-email-address"}
- response 422 (application/json)
- Attributes
- error (object)
- code: `422` (number) - Http Status Equivalent Message Code
- message (array)
- `name 은(는) 필수 입력 항목 입니다.`
- `email 은(는) 유효한 이메일 주소가 아닙니다.`
- `password 은(는) 필수 입력 항목 입니다.`
- request Duplicate User
- headers
Accept: application/json
Content-type: application/json
- body
{
"name": "John Doe",
"email": "john@example.com",
"password": "password",
"password_confirmation": "password"
}
- response 422 (application/json)
- Attributes
- error (object)
- code: `422` (number) - Http Status Equivalent Message Code
- message (array)
- `email 은(는) 이미 사용 중입니다.`
## User Login [/auth/login]
로그인을 하고 `token` 을 얻습니다.
### User Login [POST]
- request OK
- headers
Accept: application/json
Content-type: application/json
- body
{
"email": "john@example.com",
"password": "password"
}
- response 201 (application/json)
- Attributes
- success (object)
- code: `201` (number) - Http Status Equivalent Message Code
- message: `Created` (string) - Message
- meta (object)
- token: `header.payload.signature` (string) - Json Web Token
- request Invalid Credential
- headers
Accept: application/json
Content-type: application/json
- body
{
"email": "john@example.com",
"password": "wrong-password"
}
- response 401 (application/json)
- Attributes
- error (object)
- code: `401` (number) - Http Status Equivalent Message Code
- message: `invalid_credentials` (string) - Message
- request Invalid Data
- headers
Accept: application/json
Content-type: application/json
- body
{
"email": "invalid-email-address",
"password": "password"
}
email: invalid-email-address
- response 422 (application/json)
- Attributes
- error (object)
- code: `422` (number) - Http Status Equivalent Message Code
- message (array)
- `email 은(는) 유효한 이메일 주소가 아닙니다.` (string)
## Token Refresh [/auth/refresh]
만료된 `token` 을 갱신합니다.
### Token Refresh [POST]
- request OK
- headers
Accept: application/json
Authorization: Bearer header.payload.signagure
- response 201 (application/json)
- Attributes
- success (object)
- code: `201` (number) - Http Status Equivalent Message Code
- message: `Created` (string) - Message
- meta (object)
- token: `header.payload.signature` (string) - Json Web Token
# group Forum
Forum Api 는 HTTP 요청을 통해 여러분의 Api 클라이언트가 서버에 저장된 게시글 <sup>`Article`</sup> 과 댓글 <sup>`Comment`</sup> 리소스를 이용할 수 있도록 해 줍니다.
**`참고`** 댓글에 대한 생성, 수정, 삭제 Api 는 현재 구현되어 있지 않습니다.
## Articles Collection [/v1/articles]
### List All Articles [GET]
`Article` Collection 을 요청합니다. 사용할 수 있는 쿼리스트링은 다음과 같습니다.
<a name="available_querystrings"></a>
#### Available Querystrings
- `filter` - 필터 (선택, `no_comment`|`not_solved`)
```http
GET /v1/articles?filter=no_comment
```
- `limit` - 응답 받을 게시물 수 (선택, `1`~`10`, 기본값 `5`)
```http
GET /v1/articles?limit=3
```
- `sort` - 정렬에 사용할 필드 (선택, `created`|`view_count`, 기본값 `created`)
```http
GET /v1/articles?sort=view_count
```
- `order` - 정렬 방향 (선택, `asc`|`desc`, 기본값 `desc`)
```http
GET /v1/articles?sort=view_count&order=asc
```
- `q` - 풀 텍스트 서치 키워드 (선택)
```http
GET /v1/articles?q=hello%20api
```
- request OK
- headers
Accept: application/json
- response 200 (application/json)
- headers
Etag: aK08i2L8jQRdjBcd
- attributes
- data (array[Article])
- meta (Pagination)
- request Not Modified
- headers
Accept: application/json
If-None-Match: etag
- response 304 (application/json)
### Not-allowed Querystrings [GET /v1/articles?filter=not_existing_filter]
- request Not-existing Querystrings
- headers
Accept: application/json
- response 422 (application/json)
- Attributes
- error (object)
- code: `422` (number) - Http Status Equivalent Message Code
- message (object)
- filter (array)
- `filter 은(는) 유효하지 않습니다.` (string)
### Create New Article [POST]
새로운 `Article` 리소스를 생성합니다.
- request OK
- headers
Accept: application/json
Content-type: application/json
Authorization: Bearer header.payload.signagure
- body
{
"title": "some title",
"content": "some content",
"tags[]": 1,
"tags[]": 2
}
- response 201 (application/json)
- attributes
- success (object)
- code: `201` (number) - Http Status Equivalent Message Code
- message: `Created` (string) - Message
- request Token Not Provided
- headers
Accept: application/json
Content-type: application/json
- response 400 (application/json)
- attributes
- error (object)
- code: `400` (number) - Http Status Equivalent Message Code
- message: `token_not_provided` (string) - Message
- request Token Invalid
- headers
Accept: application/json
Authorization: Bearer invalid.token.signature
- response 400 (application/json)
- attributes
- error (object)
- code: `400` (number) - Http Status Equivalent Message Code
- message: `token_invalid` (string) - Message
- request Token Expired
- headers
Authorization: Bearer expired.token.signagure
Content-type: application/json
- response 401 (application/json)
- attributes
- error (object)
- code: `400` (number) - Http Status Equivalent Message Code
- message: `token_expired` (string) - Message
## Articles Instance [/v1/articles/{id}]
- parameters
- id: `95280986` (required, number)
### Fetch Article Instance [GET]
`id` 로 지정된 `Article` 리소스의 상세정보를 요청합니다.
`Article Collection` 과 동일한 [Available Querystrings](#available_querystrings) 을 사용할 수 있습니다.
- request OK
- headers
Accept: application/json
- response 200 (application/json)
- headers
Etag: 31Ba8Uuo29Za4s2v
- body
{
"id": 95280986,
"title": "example title",
"content_raw": "example content",
"content_html": "<p>example content</p>",
"created": "2015-12-19T10:37:42+0000",
"view_count": 1,
"link": {
"rel": "self",
"href": "/v1/articles/95280986"
},
"comments": 1,
"author": {
"name": "John Doe",
"email": "john@example.com",
"avatar": "http://www.gravatar.com/d4c74594d841139328695756648b6bd6"
},
"tags": [
"laravel",
"eloquent"
],
"attachments": 1
}
- request Not Modified
- headers
Accept: application/json
If-None-Match: 31Ba8Uuo29Za4s2v
- response 304 (application/json)
### Fetch Not-existing Article Instance [GET /v1/articles/{id}]
없는 `id` 이면 404 Not Found 응답을 받습니다.
- parameters
- id: `00000000` (required, number)
- request Not-existing Resource
- headers
Accept: application/json
- response 404 (application/json)
- Attributes
- error (object)
- code: `404` (number) - Http Status Equivalent Message Code
- message: `No query results for model [App\\Article].` (string) - Message
### Update Article Instance [PUT]
`id` 로 지정된 `Article` 의 내용을 수정합니다. 수정은 리소스의 소유자 또는 관리자에게만 허용되며, 이 조건이 충족되지 않을 경우에는 403 Forbidden 응답을 받습니다.
- request OK
- headers
Accept: application/json
Content-type: application/json
Authorization: Bearer header.payload.signagure
- body
{
"title": "updated title",
"content": "updated content",
"tags[]": "3"
}
- response 200 (application/json)
- attributes
- success (object)
- code: `200` (number) - Http Status Equivalent Message Code
- message: `Updated` (string) - Message
- request Forbidden
- headers
Accept: application/json
Content-type: application/json
Authorization: Bearer not.owner.signagure
- response 403 (application/json)
- attributes
- error (object)
- code: `403` (number) - Http Status Equivalent Message Code
- message: `Forbidden` (string) - Message
### Delete Article Instance [DELETE]
`id` 로 지정된 `Article` 을 삭제합니다. 삭제는 리소스의 소유자 또는 관리자에게만 허용되며, 이 조건이 충족되지 않을 경우에는 403 Forbidden 응답을 받습니다.
- request OK
- headers
Accept: application/json
Authorization: Bearer header.payload.signagure
- response 204 (application/json)
- request Forbidden
- headers
Accept: application/json
Authorization: Bearer not.owner.signagure
- response 403 (application/json)
- attributes
- error (object)
- code: `403` (number) - Http Status Equivalent Message Code
- message: `Forbidden` (string) - Message
# data structures
## Article (object)
- id: `95280986` (number) - 고유 키
- title: `example title` (string) - 제목
- content_raw: `example content` (string) - 본문 (Markdown 형식)
- content_html: `<p>example content</p>` (string) - 본문 (HTML 컴파일)
- created: `2015-12-19T10:37:42+0000` (string) - 생성일
- view_count: 1 (number) - 조회 수
- link (object)
- rel: `self`
- href: `/v1/articles/95280986` (string) - 상세 내용 요청을 위한 Api Endpoint
- comments: `1` (number) - 댓글 개수
- author (object)
- name: `John Doe` (string) - 작성자 이름
- email: `john@example.com` (string) - 작성자 이메일
- avatar: `http://www.gravatar.com/d4c74594d841139328695756648b6bd6` (string) - 작성자 아바타 URL
- tags (array)
- laravel
- eloquent
- attachments: `1` (number) - 첨부파일 개수
## User (object)
- id: `95280986` (number) - 고유 키
- name: `John Doe` (string) - 사용자 이름
- email: `john@example.com` (string) - 사용자 이메일
- avatar: `http://www.gravatar.com/d4c74594d841139328695756648b6bd6` (string) - 사용자 아바타 URL
- signup: `2016-01-12T06:18:10+0000` - 가입일
- link (object)
- rel: `self`
- href: `/users/95280986` (string) - 상세 내용 요청을 위한 Api Endpoint
- articles: `1` (number) - 포럼 게시글 개수
- comments: `1` (number) - 작성한 댓글 개수
## Comment (object)
- id: `95280986` (number) - 고유 키
- content_raw: `example content` (string) - 본문 (Markdown 형식)
- content_html: `<p>example content</p>` (string) - 본문 (HTML 컴파일)
- created: `2015-12-19T10:37:42+0000` (string) - 생성일
- vote (object)
- up: `1` (number) - 좋아요 투표 수
- down: `1` (number) - 싫어요 투표 수
- link (object)
- rel: `self`
- href: `/v1/comments/95280986` (string) - 상세 내용 요청을 위한 Api Endpoint
- author (object)
- name: `John Doe` (string) - 작성자 이름
- email: `john@example.com` (string) - 작성자 이메일
- avatar: `http://www.gravatar.com/d4c74594d841139328695756648b6bd6` (string) - 작성자 아바타 URL
## Tag (object)
- id: `95280986` (number) - 고유 키
- slug: `laravel` (string) - Slug
- created: `2015-12-19T10:37:42+0000` (string) - 생성일
- link (object)
- rel: `self`
- href: `/v1/tags/laravel/articles` (string) - 상세 내용 요청을 위한 Api Endpoint
- articles: 1 (number) - 태그에 해당하는 게시글 개수
## Attachment (object)
- id: `95280986` (number) - 고유 키
- name: `kEvzc4qBPwEze1mi.jpg`
- created: `2015-12-19T10:37:42+0000` (string) - 생성일
- link (object)
- rel: `self`
- href: `http://myproject.dev:8000/attachments/kEvzc4qBPwEze1mi.jpg` (string) - 첨부파일 다운로드 URL
## Pagination
- pagination (object)
- total: `20` (number) - 전체 게시글 개수
- count: `5` (number) - 현재 응답의 게시글 개수
- per_page: `5` (number) - 요청당 응답할 게시글 개수
- current_page: `2` (number) - 현재 페이지 번호
- total_pages: `4` (number) - 총 페이지 수
- links (object)
- previous: `/v1/articles?page=1` (string) - 이전 페이지 URL
- next: `/v1/articles?page=3` (string) - 다음 페이지 URL
================================================
FILE: app/Article.php
================================================
<?php
namespace App;
use Illuminate\Database\Eloquent\SoftDeletes;
class Article extends Model
{
use SoftDeletes;
use AuthorTrait;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'author_id',
'title',
'content',
'notification',
'solution_id',
'pin',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'author_id',
'solution_id',
'notification',
'deleted_at',
'pin',
];
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = [
'author',
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'deleted_at'
];
/* Relationships */
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
public function tags()
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
public function solution()
{
return $this->hasOne(Comment::class, 'id', 'solution_id');
}
public function attachments()
{
return $this->hasMany(Attachment::class);
}
/* Query Scope */
public function scopeNoComment($query)
{
return $query->has('comments', '<', 1);
}
public function scopeNotSolved($query)
{
return $query->whereNull('solution_id');
}
/* Helpers */
public function isNotice()
{
return $this->pin ? true : false;
}
public function etag($cacheKey = null)
{
$etag = $this->getTable() . $this->getKey();
if ($this->usesTimestamps()) {
$etag .= $this->updated_at->timestamp;
}
return md5($etag.$cacheKey);
}
}
================================================
FILE: app/Attachment.php
================================================
<?php
namespace App;
class Attachment extends Model
{
protected $fillable = [
'name'
];
protected $hidden = [
'article_id'
];
/* Relationships */
public function article()
{
return $this->belongsTo(Article::class);
}
}
================================================
FILE: app/AuthorTrait.php
================================================
<?php
namespace App;
trait AuthorTrait
{
/**
* Determine if the current instance was authored by the current user.
*
* @return bool
*/
public function isAuthor()
{
return $this->author->id == auth()->user()->id;
}
}
================================================
FILE: app/Comment.php
================================================
<?php
namespace App;
use Illuminate\Database\Eloquent\SoftDeletes;
class Comment extends Model
{
use SoftDeletes;
use AuthorTrait;
protected $fillable = [
'commentable_type',
'commentable_id',
'author_id',
'parent_id',
'title',
'content'
];
protected $hidden = [
'author_id',
'commentable_type',
'commentable_id',
'parent_id',
'deleted_at',
];
protected $dates = [
'deleted_at'
];
protected $with = [
'author',
'votes',
];
protected $appends = [
'up_count',
'down_count'
];
/* Accessors */
public function getUpCountAttribute()
{
return (int) static::votes()->sum('up');
}
public function getDownCountAttribute()
{
return (int) static::votes()->sum('down');
}
/* Relationships */
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
public function commentable()
{
return $this->morphTo();
}
public function replies()
{
return $this->hasMany(Comment::class, 'parent_id')->latest();
}
public function parent()
{
return $this->belongsTo(Comment::class, 'parent_id', 'id');
}
public function votes()
{
return $this->hasMany(Vote::class);
}
}
================================================
FILE: app/Console/Commands/BackupDb.php
================================================
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class BackupDb extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'my:backup-db
{user : User name for database login.}
{pass : Password for database login.}
{--S|db=myProject : Database name to backup.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Execute database backup.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$dir = storage_path('backup');
if (! \File::isDirectory($dir)) {
\File::makeDirectory($dir);
}
$command = sprintf(
'mysqldump %s > %s -u%s -p%s',
$this->option('db'),
storage_path("backup/{$this->option('db')}.sql"),
$this->argument('user'),
$this->argument('pass')
);
system($command);
$now = \Carbon\Carbon::now()->toDateTimeString();
$result = "{$this->getName()} command done at {$now}";
\Log::info($result);
return $this->info($result);
}
}
================================================
FILE: app/Console/Commands/ClearLog.php
================================================
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ClearLog extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'my:clear-log';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear Laravel log.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$path = storage_path('logs/laravel.log');
system('cat /dev/null > ' . $path);
$now = \Carbon\Carbon::now()->toDateTimeString();
$result = "{$this->getName()} command done at {$now}";
\Log::info($result);
return $this->info($result);
}
}
================================================
FILE: app/Console/Commands/Inspire.php
================================================
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
class Inspire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}
================================================
FILE: app/Console/Commands/PruneRelease.php
================================================
<?php
namespace App\Console\Commands;
use File;
use Illuminate\Console\Command;
class PruneRelease extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'my:prune-release
{path : Releases path.}
{--K|keep=3 : Number of recent releases to keep.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune old releases.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$path = $this->argument('path');
$dirs = collect(File::directories($path))->sortByDesc(function($dir) {
return File::lastModified($dir);
});
$dirs->values()->splice($this->option($keep))->map(function($dir) {
File::deleteDirectory($dir);
$this->info(sprintf('%s removed.', $dir));
});
$now = \Carbon\Carbon::now()->toDateTimeString();
$result = sprintf(
'%s command done at %s. %d %s removed',
$this->getName(),
$now,
$dirs->count(),
str_plural('release', $dirs->count())
);
\Log::info($result . ': ' . $dirs->toJson());
return $this->warn($result);
}
}
================================================
FILE: app/Console/Commands/UpdateLessonsTable.php
================================================
<?php
namespace App\Console\Commands;
use App\DocumentRepository;
use Illuminate\Console\Command;
class UpdateLessonsTable extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'my:update-lessons {--A|all=true : If true, all lesson files be processed}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update the content of the lessons table.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$lessons = \App\Lesson::all();
foreach($lessons as $lesson) {
$path = base_path(\App\Lesson::$path . DIRECTORY_SEPARATOR . $lesson->name);
$lesson->content = \File::get($path);
$lesson->save();
$lesson->touch();
$this->info(sprintf('Success updating %d: %s', $lesson->id, $lesson->name));
}
return $this->warn('Finished.');
}
}
================================================
FILE: app/Console/Kernel.php
================================================
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
\App\Console\Commands\Inspire::class,
\App\Console\Commands\UpdateLessonsTable::class,
\App\Console\Commands\BackupDb::class,
\App\Console\Commands\ClearLog::class,
\App\Console\Commands\PruneRelease::class,
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')->hourly();
$schedule->command('my:clear-log')->monthly();
$schedule->command(sprintf(
'my:backup-db %s %s',
env('DB_USERNAME'),
env('DB_PASSWORD')
))->dailyAt('03:00');
}
}
================================================
FILE: app/Events/ArticleConsumed.php
================================================
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
class ArticleConsumed extends Event
{
use SerializesModels;
/**
* @var \App\Article
*/
public $article;
/**
* Create a new event instance.
*
* @param \App\Article $article
*/
public function __construct(\App\Article $article)
{
$this->article = $article;
}
}
================================================
FILE: app/Events/Event.php
================================================
<?php
namespace App\Events;
abstract class Event
{
//
}
================================================
FILE: app/Events/ModelChanged.php
================================================
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
class ModelChanged extends Event
{
use SerializesModels;
/**
* @var string|array
*/
public $cacheTags;
/**
* Create a new event instance.
*
* @param string $cacheTags
*/
public function __construct($cacheTags)
{
$this->cacheTags = $cacheTags;
}
}
================================================
FILE: app/Exceptions/Handler.php
================================================
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Foundation\Validation\ValidationException;
use Illuminate\Http\Exception\HttpResponseException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
NotFoundHttpException::class,
ValidationException::class,
TokenExpiredException::class,
TokenInvalidException::class,
JWTException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $e
* @return void
*/
public function report(Exception $e)
{
if ($this->shouldReport($e) and app()->environment('production')) {
app(\App\Reporters\ErrorReport::class, [$e])->send();
}
return parent::report($e);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if (app()->environment('production')) {
$title = 'Error';
$description = 'Unknown error occurred :(';
$statusCode = 400;
if ($e instanceof ModelNotFoundException or $e instanceof NotFoundHttpException) {
$title = trans('errors.not_found');
$description = trans('errors.not_found_description');
$statusCode = 404;
}
return response(view('errors.notice', [
'title' => $title,
'description' => $description
]), $e->getCode() ?: $statusCode);
}
if (is_api_request()) {
$statusCode = method_exists($e, 'getStatusCode')
? $e->getStatusCode()
: $e->getCode();
if ($e instanceof TokenExpiredException) {
$message = 'token_expired';
} elseif ($e instanceof TokenInvalidException) {
$message = 'token_invalid';
} elseif ($e instanceof JWTException) {
$message = $e->getMessage() ?: 'could_not_create_token';
} elseif ($e instanceof NotFoundHttpException or $e instanceof ModelNotFoundException) {
$statusCode = 404;
$message = $e->getMessage() ?: 'not_found';
} elseif ($e instanceof MethodNotAllowedHttpException) {
$message = $e->getMessage() ?: 'not_allowed';
} elseif ($e instanceof HttpResponseException){
return $e->getResponse();
} elseif ($e instanceof Exception){
$message = $e->getMessage() ?: 'Whoops~ Tell me what you did :(';
}
return json()->setStatusCode($statusCode ?: 400)->error($message);
}
return parent::render($request, $e);
}
}
================================================
FILE: app/Http/Controllers/Api/PasswordsController.php
================================================
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\PasswordsController as ParentController;
class PasswordsController extends ParentController
{
public function __construct()
{
// Kill middleware defined by ParentController.
// $this->middleware = [];
$this->middleware('throttle.api:10,1');
parent::__construct();
}
/**
* Make an error response.
*
* @param $message
* @param int $statusCode
* @return \Illuminate\Http\JsonResponse
*/
protected function respondError($message, $statusCode = 400)
{
return json()->setStatusCode($statusCode)->error('not_found');
}
/**
* Make a success response.
*
* @param $message
* @return \Illuminate\Http\RedirectResponse
*/
protected function respondSuccess($message)
{
return json()->success();
}
}
================================================
FILE: app/Http/Controllers/Api/SessionsController.php
================================================
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\SessionsController as ParentController;
use Illuminate\Contracts\Validation\Validator;
class SessionsController extends ParentController
{
public function __construct()
{
// Kill middleware defined by ParentController.
// $this->middleware = [];
$this->middleware('throttle.api:10,1');
$this->middleware('jwt.refresh', ['only' => 'refresh']);
parent::__construct();
}
/**
* Blank method for token refresh.
*
* @return bool
*/
public function refresh()
{
return true;
}
/**
* Make validation error response.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return \Illuminate\Http\JsonResponse
*/
protected function respondValidationError(Validator $validator)
{
return json()->unprocessableError($validator->errors()->all());
}
/**
* Make login failed response.
*
* @return \Illuminate\Http\JsonResponse
*/
protected function respondLoginFailed()
{
return json()->unauthorizedError('invalid_credentials');
}
/**
* Make a success response.
*
* @param string $return
* @param string $token
* @return \Illuminate\Http\JsonResponse
*/
protected function respondCreated($return = '', $token = '')
{
return json()->setMeta(['token' => $token])->created();
}
}
================================================
FILE: app/Http/Controllers/Api/UsersController.php
================================================
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\UsersController as ParentController;
use App\User;
use Illuminate\Contracts\Validation\Validator;
class UsersController extends ParentController
{
public function __construct()
{
// Kill middleware defined by ParentController.
// $this->middleware = [];
$this->middleware('throttle.api:10,1');
parent::__construct();
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
return json()->withItem(
\App\User::with('articles', 'comments')->findOrFail($id),
new \App\Transformers\UserTransformer
);
}
/**
* Make validation error response.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return \Illuminate\Http\JsonResponse
*/
protected function respondValidationError(Validator $validator)
{
return json()->unprocessableError($validator->errors()->all());
}
/**
* Make a success response.
*
* @param \App\User $user
* @return \Illuminate\Http\JsonResponse
*/
protected function respondCreated(User $user)
{
return json()->setMeta(['token' => \JWTAuth::fromUser($user)])->created();
}
}
================================================
FILE: app/Http/Controllers/Api/V1/ArticlesController.php
================================================
<?php
namespace App\Http\Controllers\Api\V1;
use App\Article;
use App\Transformers\ArticleTransformer;
use App\Http\Controllers\ArticlesController as ParentController;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
class ArticlesController extends ParentController
{
public function __construct()
{
$this->middleware('jwt.auth', ['except' => ['index', 'show']]);
$this->middleware('throttle.api:60,1');
$this->middleware('obfuscate:article');
parent::__construct();
}
/**
* Respond Article collection in JSON.
*
* @param \Illuminate\Pagination\LengthAwarePaginator $articles
* @param string|null $cacheKey
* @return \Illuminate\Http\JsonResponse
*/
protected function respondCollection(LengthAwarePaginator $articles, $cacheKey = null)
{
$reqEtag = request()->getETags();
$genEtag = $this->etags($articles, $cacheKey);
if (config('project.cache') === true and isset($reqEtag[0]) and $reqEtag[0] === $genEtag) {
return $this->respondNotModified();
}
return json()->setHeaders(['Etag' => $genEtag])->withPagination(
$articles,
new ArticleTransformer
);
}
/**
* Respond 201 in JSON.
*
* @param \App\Article $article
* @return \Illuminate\Http\JsonResponse
*/
protected function respondCreated(Article $article)
{
return json()->created();
}
/**
* Respond single Article item in JSON.
*
* @param \App\Article $article
* @param \Illuminate\Database\Eloquent\Collection|null $commentsCollection
* @param string|null $cacheKey
* @return \Illuminate\Http\JsonResponse
*/
protected function respondItem(Article $article, Collection $commentsCollection = null, $cacheKey = null)
{
$reqEtag = request()->getETags();
$genEtag = $article->etag($cacheKey);
if (config('project.cache') === true and isset($reqEtag[0]) and $reqEtag[0] === $genEtag) {
return $this->respondNotModified();
}
return json()->setHeaders(['Etag' => $genEtag])->withItem($article, new ArticleTransformer);
}
/**
* Respond Updated in JSON.
*
* @param \App\Article $article
* @return \Illuminate\Http\JsonResponse
*/
protected function respondUpdated(Article $article)
{
return json()->success('Updated');
}
/**
* Respond 204 Deleted.
*
* @param \App\Article $article
* @return \Illuminate\Http\JsonResponse
*/
protected function respondDeleted(Article $article)
{
return json()->noContent();
}
/**
* Respond Not Modified;
*
* @return \Illuminate\Contracts\Http\Response
*/
protected function respondNotModified()
{
return json()->notModified();
}
}
================================================
FILE: app/Http/Controllers/Api/V1/CommentsController.php
================================================
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Transformers\CommentTransformer;
class CommentsController extends Controller
{
public function __construct()
{
// $this->middleware('jwt.auth');
parent::__construct();
}
/**
* Display a listing of the resource.
*/
public function index()
{
return json()->withPagination(
\App\Comment::paginate(5),
new CommentTransformer
);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$article = \App\Comment::findOrFail($id);
return json()->withItem(
$article,
new CommentTransformer
);
}
}
================================================
FILE: app/Http/Controllers/Api/V1/WelcomeController.php
================================================
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
class WelcomeController extends Controller
{
/**
* Get the index page
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
return json([
'name' => 'myProject Api',
'message' => 'Welcome to myProject Api. This is a base endpoint of version 1.',
'version' => 'v1',
'links' => [
[
'rel' => 'self',
'href' => route(\Route::currentRouteName())
],
[
'rel' => 'api.users.store',
'href' => route('api.users.store')
],
[
'rel' => 'api.sessions.store',
'href' => route('api.sessions.store')
],
[
'rel' => 'api.v1.docs',
'href' => 'http://docs.forumv1.apiary.io/'
],
],
]);
}
}
================================================
FILE: app/Http/Controllers/Api/WelcomeController.php
================================================
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
class WelcomeController extends Controller
{
/**
* Get the index page
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
return json([
'name' => 'myProject Api',
'message' => 'Welcome to myProject Api. This is a base endpoint.',
'version' => 'n/a',
'links' => [
[
'rel' => 'self',
'href' => route(\Route::currentRouteName())
],
[
'rel' => 'api.users.store',
'href' => route('api.users.store')
],
[
'rel' => 'api.sessions.store',
'href' => route('api.sessions.store')
],
[
'rel' => 'api.v1.index',
'href' => route('api.v1.index')
],
[
'rel' => 'api.v1.docs',
'href' => 'http://docs.forumv1.apiary.io/'
],
],
]);
}
}
================================================
FILE: app/Http/Controllers/ArticlesController.php
================================================
<?php
namespace App\Http\Controllers;
use App\Article;
use App\Events\ArticleConsumed;
use App\Events\ModelChanged;
use App\Http\Requests\ArticlesRequest;
use App\Http\Requests\FilterArticlesRequest;
use App\Tag;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
class ArticlesController extends Controller implements Cacheable
{
public function __construct()
{
$this->middleware('author:article', ['only' => ['update', 'destroy', 'pickBest']]);
if (! is_api_request()) {
$this->middleware('auth', ['except' => ['index', 'show']]);
$allTags = \Cache::remember('tags', 30, function() {
return Tag::with('articles')->get();
});
view()->share('allTags', $allTags);
}
parent::__construct();
}
public function cacheKeys()
{
return 'articles';
}
/**
* Display a listing of the resource.
*
* @param \App\Http\Requests\FilterArticlesRequest $request
* @param string|null $slug
* @return \Illuminate\Http\Response
*/
public function index(FilterArticlesRequest $request, $slug = null)
{
$query = $slug
? Tag::whereSlug($slug)->firstOrFail()->articles()
: new Article;
$cacheKey = cache_key('articles.index');
$query = $this->filter($query->orderBy('pin', 'desc'));
$args = $request->input(config('project.params.limit'), 5);
$articles = $this->cache($cacheKey, 5, $query, 'paginate', $args);
return $this->respondCollection($articles, $cacheKey);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
$article = new Article;
return view('articles.create', compact('article'));
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\ArticlesRequest $request
* @return \Illuminate\Http\Response
*/
public function store(ArticlesRequest $request)
{
$payload = array_merge($request->except('_token'), [
'notification' => $request->has('notification'),
]);
$article = $request->user()->articles()->create($payload);
$article->tags()->sync($request->input('tags'));
if ($request->has('attachments')) {
$attachments = \App\Attachment::whereIn('id', $request->input('attachments'))->get();
$attachments->each(function ($attachment) use ($article) {
$attachment->article()->associate($article);
$attachment->save();
});
}
event(new ModelChanged(['articles', 'tags']));
return $this->respondCreated($article);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
$cacheKey = cache_key("articles.show.{$id}");
$secondKey = cache_key("articles.show.{$id}.comments");
$query = Article::with('comments', 'tags', 'attachments', 'solution')->findOrFail($id);
$article = $this->cache($cacheKey, 5, $query, 'findOrFail', $id);
$secondQuery = $article->comments()->with('replies')->withTrashed()->whereNull('parent_id')->latest();
$commentsCollection = $this->cache($secondKey, 5, $secondQuery, 'get');
if (! is_api_request()) {
event(new ArticleConsumed($article));
}
return $this->respondItem($article, $commentsCollection, $cacheKey.$secondKey);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
$article = Article::findOrFail($id);
return view('articles.edit', compact('article'));
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\ArticlesRequest $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(ArticlesRequest $request, $id)
{
$payload = array_merge($request->except('_token'), [
'notification' => $request->has('notification'),
]);
$article = Article::findOrFail($id);
$article->update($payload);
if ($request->has('tags')) {
$article->tags()->sync($request->input('tags'));
}
event(new ModelChanged(['articles', 'tags']));
return $this->respondUpdated($article);
}
public function pickBest(Request $request, $id)
{
$this->validate($request, [
'solution_id' => 'required|numeric|exists:comments,id',
]);
Article::findOrFail($id)->update([
'solution_id' => $request->input('solution_id'),
]);
return json()->noContent();
}
/**
* Remove the specified resource from storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
* @throws \Exception
*/
public function destroy(Request $request, $id)
{
$article = Article::with('attachments', 'comments')->findOrFail($id);
foreach ($article->attachments as $attachment) {
\File::delete(attachment_path($attachment->name));
}
$article->attachments()->delete();
$article->comments->each(function ($comment) use ($request) {
app(\App\Http\Controllers\CommentsController::class)->destroy($request, $comment->id);
});
$article->delete();
event(new ModelChanged('articles'));
if ($request->ajax()) {
return response()->json('', 204);
}
return $this->respondDeleted($article);
}
/**
* Respond Article Collection.
*
* @param \Illuminate\Pagination\LengthAwarePaginator $articles
* @param string|null $cacheKey
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
protected function respondCollection(LengthAwarePaginator $articles, $cacheKey = null)
{
return view('articles.index', compact('articles'));
}
/**
* Respond Created.
*
* @param \App\Article $article
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function respondCreated(Article $article)
{
flash()->success(trans('common.created'));
return redirect(route('articles.index'));
}
/**
* Respond single Article item with a corresponding Comment collection.
*
* @param \App\Article $article
* @param \Illuminate\Database\Eloquent\Collection|null $commentsCollection
* @param string|null $cacheKey
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
protected function respondItem(Article $article, Collection $commentsCollection = null, $cacheKey = null)
{
return view('articles.show', [
'article' => $article,
'comments' => $commentsCollection,
'commentableType' => Article::class,
'commentableId' => $article->id,
]);
}
/**
* Respond Updated.
*
* @param \App\Article $article
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function respondUpdated(Article $article)
{
flash()->success(trans('common.updated'));
return redirect(route('articles.show', $article->id));
}
/**
* Respond Deleted.
*
* @param \App\Article $article
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function respondDeleted(Article $article)
{
flash()->success(trans('common.deleted'));
return redirect(route('articles.index'));
}
/**
* Respond Not Modified.
*
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
protected function respondNotModified()
{
return response('', 304);
}
}
================================================
FILE: app/Http/Controllers/AttachmentsController.php
================================================
<?php
namespace App\Http\Controllers;
use App\Events\ModelChanged;
use Illuminate\Http\Request;
use App\Http\Requests;
class AttachmentsController extends Controller
{
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
if (! $request->hasFile('file')) {
return response()->json('File not passed !', 422);
}
// Save file
$file = $request->file('file');
$name = time() . '_' . str_replace(' ', '_', $file->getClientOriginalName());
$file->move(attachment_path(), $name);
$articleId = $request->input('articleId');
// Persist Attachment model
$attachment = $articleId
? \App\Article::findOrFail($articleId)->attachments()->create(['name' => $name])
: \App\Attachment::create(['name' => $name]);
event(new ModelChanged('attachments'));
return response()->json([
'id' => $attachment->id,
'name' => $name,
'type' => $file->getClientMimeType(),
'url' => sprintf("/attachments/%s", $name),
]);
}
/**
* Remove the specified resource from storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, $id)
{
$attachment = \App\Attachment::findOrFail($id);
$path = attachment_path($attachment->name);
if (\File::exists($path)) {
\File::delete($path);
}
$attachment->delete();
event(new ModelChanged('attachments'));
if ($request->ajax()) {
return response()->json('', 204);
}
flash()->success(trans('common.deleted'));
return back();
}
}
================================================
FILE: app/Http/Controllers/Cacheable.php
================================================
<?php
namespace App\Http\Controllers;
interface Cacheable
{
/**
* Specify the tags for caching.
* @see https://laravel.com/docs/cache#cache-tags
*
* @return string
*/
public function cacheKeys();
}
================================================
FILE: app/Http/Controllers/CommentsController.php
================================================
<?php
namespace App\Http\Controllers;
use App\Comment;
use App\Vote;
use App\Events\ModelChanged;
use Illuminate\Http\Request;
class CommentsController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('author:comment', ['except' => ['store', 'vote']]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate($request, [
'commentable_type' => 'required|in:App\Article,App\Lesson',
'commentable_id' => 'required|numeric',
'parent_id' => 'numeric|exists:comments,id',
'content' => 'required',
]);
$parentModel = "\\" . $request->input('commentable_type');
$comment = $parentModel::find($request->input('commentable_id'))
->comments()->create([
'author_id' => \Auth::user()->id,
'parent_id' => $request->input('parent_id', null),
'content' => $request->input('content')
]);
event('comments.created', [$comment]);
event(new ModelChanged('articles', 'comments'));
flash()->success(trans('forum.comment_add'));
return back();
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
$this->validate($request, ['content' => 'required']);
$comment = Comment::findOrFail($id);
$comment->update($request->only('content'));
event('comments.updated', [$comment]);
event(new ModelChanged('articles', 'comments'));
flash()->success(trans('forum.comment_edit'));
return back();
}
/**
* Vote up or down for the given comment.
*
* @param \Illuminate\Http\Request $request
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function vote(Request $request, $id)
{
$this->validate($request, [
'vote' => 'required|in:up,down',
]);
if(Vote::whereCommentId($id)->whereUserId($request->user()->id)->exists()) {
return response()->json(['errors' => 'Already voted!'], 409);
}
$comment = Comment::findOrFail($id);
$up = $request->input('vote') == 'up' ? true : false;
$comment->votes()->create([
'user_id' => $request->user()->id,
'up' => $up ? 1 : null,
'down' => $up ? null : 1,
'voted_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
return response()->json([
'voted' => $request->input('vote'),
'value' => $comment->votes()->sum($request->input('vote'))
]);
}
/**
* Remove the specified resource from storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, $id)
{
$comment = Comment::with('replies')->find($id);
// Do not recursively destroy children comments.
// Because 1. Soft delete feature was adopted,
// and 2. it's not just pleasant for authors of children comments to being deleted by the parent author.
if ($comment->replies->count() > 0) {
$comment->delete();
} else {
if ($comment->votes->count()) {
$this->deleteVote($comment->votes);
}
$comment->forceDelete();
}
// $this->recursiveDestroy($comment);
event(new ModelChanged('articles', 'comments'));
if ($request->ajax()) {
return response()->json('', 204);
}
flash()->success(trans('common.deleted'));
return back();
}
/**
* Delete given votes collection.
*
* @param $votes
*/
protected function deleteVote($votes)
{
foreach($votes as $vote) {
$vote->delete();
}
}
/**
* Delete comment recursively
*
* @param \App\Comment $comment
* @return bool|null
*/
public function recursiveDestroy(Comment $comment)
{
if ($comment->replies->count()) {
$comment->replies->each(function($reply) {
if ($reply->replies->count()) {
$this->recursiveDestroy($reply);
} else {
$reply->delete();
}
});
}
return $comment->delete();
}
}
================================================
FILE: app/Http/Controllers/Controller.php
================================================
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/**
* @var \Illuminate\Cache\CacheManager
*/
protected $cache;
/**
* Constructor
*/
public function __construct() {
if (! is_api_request()) {
$this->setSharedVariables();
}
$this->cache = app('cache');
if ((new \ReflectionClass($this))->implementsInterface(\App\Http\Controllers\Cacheable::class) and taggable()) {
$this->cache = app('cache')->tags($this->cacheKeys());
}
}
/**
* Share common view variables
*/
protected function setSharedVariables() {
view()->share('currentLocale', app()->getLocale());
view()->share('currentUser', auth()->user());
view()->share('currentRouteName', \Route::currentRouteName());
view()->share('currentUrl', current_url());
view()->share('isLandingPage', in_array(\Route::currentRouteName(), ['home', 'index']));
}
/**
* Do the filter, search, and sorting job
*
* @param $query
* @return mixed
*/
protected function filter($query)
{
$params = config('project.params');
if ($filter = request()->input($params['filter'])) {
$query = $query->{camel_case($filter)}();
}
if ($keyword = request()->input($params['search'])) {
$raw = 'MATCH(title,content) AGAINST(? IN BOOLEAN MODE)';
$query = $query->whereRaw($raw, [$keyword]);
}
$sort = request()->input($params['sort'], 'created_at');
$direction = request()->input($params['order'], 'desc');
if ($sort == 'created') {
// We transformed field name of 'created_at' to 'created'.
// Applicable only to api request. But this code laid here
// to suppress QueryException of not existing column in web request.
$sort = 'created_at';
}
return $query->orderBy($sort, $direction);
}
/**
* Create etag against collection of resources.
*
* @param \Illuminate\Database\Eloquent\Collection|\Illuminate\Contracts\Pagination\Paginator| $collection
* @param string|null $cacheKey
* @return string
*/
protected function etags($collection, $cacheKey = null)
{
$etag = '';
foreach($collection as $instance) {
$etag .= $instance->etag();
}
return md5($etag.$cacheKey);
}
/**
* Execute caching against database query.
*
* @see config/project.php's cache section.
*
* @param string $key
* @param int $minutes
* @param \App\Model|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
* |\Illuminate\Database\Eloquent\Relations\Relation $query
* @param string $method
* @param mixed ...$args
* @return mixed
*/
protected function cache($key, $minutes, $query, $method, ...$args)
{
$args = (! empty($args)) ? implode(',', $args) : null;
if (config('project.cache') === false) {
return $query->{$method}($args);
}
return $this->cache->remember($key, $minutes, function() use($query, $method, $args){
return $query->{$method}($args);
});
}
}
================================================
FILE: app/Http/Controllers/LessonsController.php
================================================
<?php
namespace App\Http\Controllers;
use App\Document;
use App\Repositories\LessonRepository;
use Request;
class LessonsController extends Controller
{
/**
* @var \App\Repositories\LessonRepository
*/
protected $repo;
/**
* Constructor.
*
* @param \App\Repositories\LessonRepository $repo
*/
public function __construct(LessonRepository $repo)
{
$this->repo = $repo;
parent::__construct();
}
/**
* Show document page in response to the given $file.
*
* @param string $file
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function show($file = '01-welcome.md')
{
$lesson = $this->repo->find($file);
$commentsCollection = $lesson->comments()->with('replies')
->withTrashed()->whereNull('parent_id')->latest()->get();
return view('lessons.show', [
'index' => $this->repo->index(),
'lesson' => $lesson,
'prev' => $this->repo->prev($file),
'next' => $this->repo->next($file),
'comments' => $commentsCollection,
'commentableType' => $this->repo->model(),
'commentableId' => $lesson->id,
]);
}
/**
* Make image response.
*
* @param $file
* @return \Illuminate\Http\Response
*/
public function image($file)
{
$image = $this->repo->image($file);
$reqEtag = Request::getEtags();
$genEtag = $this->repo->etag($file);
if (isset($reqEtag[0])) {
if ($reqEtag[0] === $genEtag) {
return response('', 304);
}
}
return response($image->encode('png'), 200, [
'Content-Type' => 'image/png',
'Cache-Control' => 'public, max-age=0',
'Etag' => $genEtag,
]);
}
/**
* Leave off unnecessary string from the given path.
*
* @param $path
* @return mixed
*/
protected function sanitizePath($path)
{
return starts_with($path, ['/lessons/', 'lessons/'])
? pathinfo($path, PATHINFO_BASENAME)
: $path;
}
}
================================================
FILE: app/Http/Controllers/PasswordsController.php
================================================
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use Password;
class PasswordsController extends Controller
{
/**
* Create new password controller instance.
*/
public function __construct()
{
$this->middleware('guest');
parent::__construct();
}
/**
* Display the form to request a password reset link.
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function getRemind()
{
return view('passwords.remind');
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
* @return $this|\Illuminate\Http\RedirectResponse
*/
public function postRemind(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
if (User::whereEmail($request->input('email'))->noPassword()->first()) {
// Notify the user if he/she is a social login user.
$message = sprintf("%s %s", trans('auth.social_olny'), trans('auth.no_password'));
return $this->respondError($message, 400);
}
$response = Password::sendResetLink($request->only('email'), function ($m) {
$m->subject(trans('auth.email_password_reset_title'));
});
switch ($response) {
case Password::RESET_LINK_SENT:
return $this->respondSuccess(trans($response));
case Password::INVALID_USER:
return $this->respondError(trans($response), 404);
}
}
/**
* Display the password reset view for the given token.
*
* @param null $token
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function getReset($token = null)
{
if (is_null($token) or strlen($token) != 64) {
throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
}
return view('passwords.reset', compact('token'));
}
/**
* Reset the given user's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function postReset(Request $request)
{
$this->validate($request, [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed',
]);
$credentials = $request->only(
'email',
'password',
'password_confirmation',
'token'
);
$response = Password::reset($credentials, function ($user, $password) {
$user->password = bcrypt($password);
$user->save();
\Auth::login($user);
});
switch ($response) {
case Password::PASSWORD_RESET:
flash(sprintf(
"%s %s",
trans($response),
trans('auth.welcome', ['name' => \Auth::user()->name])
));
return redirect(route('home'));
default:
flash()->error(trans($response));
return back()
->withInput($request->only('email'));
}
}
/**
* Make an error response.
*
* @param $message
* @param int $statusCode
* @return \Illuminate\Http\RedirectResponse
*/
protected function respondError($message, $statusCode = 400)
{
flash()->error($message);
return back()->withInput();
}
/**
* Make a success response.
*
* @param $message
* @return \Illuminate\Http\RedirectResponse
*/
protected function respondSuccess($message)
{
flash($message);
return back();
}
}
================================================
FILE: app/Http/Controllers/SessionsController.php
================================================
<?php
namespace App\Http\Controllers;
use Auth;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Request;
class SessionsController extends Controller
{
/**
* Create a new session controller instance.
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'destroy']);
parent::__construct();
}
/**
* Show the application login form.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('sessions.create');
}
/**
* Handle login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function store(Request $request)
{
$validator = \Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required|min:6',
]);
if ($validator->fails()) {
return $this->respondValidationError($validator);
}
$token = is_api_request()
? \JWTAuth::attempt($request->only('email', 'password'))
: Auth::attempt($request->only('email', 'password'), $request->has('remember'));
if (! $token) {
return $this->respondLoginFailed();
}
event('users.login', [Auth::user()]);
return $this->respondCreated($request->input('return'), $token);
}
/**
* Log the user out of the application.
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function destroy()
{
Auth::logout();
flash(trans('auth.goodbye'));
return redirect(route('index'));
}
/**
* Make validation error response.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return \Illuminate\Http\RedirectResponse
*/
protected function respondValidationError(Validator $validator)
{
return back()->withInput()->withErrors($validator);
}
/**
* Make login failed response.
*
* @return \Illuminate\Http\RedirectResponse
*/
protected function respondLoginFailed()
{
flash()->error(trans('auth.failed'));
return back()->withInput();
}
/**
* Make a success response.
*
* @param string $return
* @param string $token
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function respondCreated($return = '', $token = '')
{
flash(trans('auth.welcome', ['name' => Auth::user()->name]));
return ($return)
? redirect(urldecode($return))
: redirect()->intended();
}
}
================================================
FILE: app/Http/Controllers/SocialController.php
================================================
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use Laravel\Socialite\Contracts\Factory as Socialite;
class SocialController extends Controller
{
/**
* @var Factory
*/
private $socialite;
/**
* Create social login controller instance.
*
* @param Socialite $socialite
*/
public function __construct(Socialite $socialite)
{
$this->middleware('guest', ['only' => 'execute']);
$this->socialite = $socialite;
parent::__construct();
}
/**
* Handle social login process.
*
* @param \Illuminate\Http\Request $request
* @param string $provider
* @return \App\Http\Controllers\Response
*/
public function execute(Request $request, $provider)
{
if (! $request->has('code')) {
return $this->redirectToProvider($provider);
}
return $this->handleProviderCallback($provider);
}
/**
* Redirect the user to the Social Login Provider's authentication page.
*
* @param string $provider
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
protected function redirectToProvider($provider)
{
return $this->socialite->driver($provider)->redirect();
}
/**
* Obtain the user information from the Social Login Provider.
*
* @param string $provider
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function handleProviderCallback($provider)
{
$user = $this->socialite->driver($provider)->user();
$user = (User::whereEmail($user->getEmail())->first())
?: User::create([
'name' => $user->getName() ?: 'unknown',
'email' => $user->getEmail(),
]);
\Auth::login($user, true);
event('users.login', [$user]);
flash(trans('auth.welcome', ['name' => $user->name]));
return redirect(route('home'));
}
}
================================================
FILE: app/Http/Controllers/UsersController.php
================================================
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Request;
class UsersController extends Controller
{
/**
* Create user controller instance.
*/
public function __construct()
{
$this->middleware('guest');
parent::__construct();
}
/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('users.create');
}
/**
* Handle a registration request for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
if ($user = User::whereEmail($request->input('email'))->noPassword()->first()) {
// Filter through the User model to find whether there is a social account
// that has the same email address with the current request
return $this->syncAccountInfo($request, $user);
}
return $this->createAccount($request);
}
/**
* A user logged into the application with social account first,
* and then, when s/he tries to register an application's native account,
* update his/her name and password as given
*
* @param \Illuminate\Http\Request $request
* @param \App\User $user
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function syncAccountInfo(Request $request, User $user)
{
$validator = \Validator::make($request->except('_token'), [
'name' => 'required|max:255',
'email' => 'required|email|max:255',
'password' => 'required|confirmed|min:6',
]);
if ($validator->fails()) {
return $this->respondValidationError($validator);
}
$user->update([
'name' => $request->input('name'),
'password' => bcrypt($request->input('password')),
]);
$this->addMemberRole($user);
return $this->respondCreated($user);
}
/**
* A user tries to register a native account.
* S/he haven't logged in to the application with a social account before.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function createAccount(Request $request)
{
$validator = \Validator::make($request->except('_token'), [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed|min:6',
]);
if ($validator->fails()) {
return $this->respondValidationError($validator);
}
$user = User::create([
'name' => $request->input('name'),
'email' => $request->input('email'),
'password' => bcrypt($request->input('password')),
]);
$this->addMemberRole($user);
return $this->respondCreated($user);
}
/**
* Attach Role to the user
*
* @param \App\User $user
* @return array
*/
protected function addMemberRole(User $user)
{
// 1 is admin, 2 is member
return $user->roles()->sync([2]);
}
/**
* Make validation error response.
*
* @param $validator
* @return \Illuminate\Http\RedirectResponse
*/
protected function respondValidationError(Validator $validator)
{
return back()->withInput()->withErrors($validator);
}
/**
* Make a success response.
*
* @param \App\User $user
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
protected function respondCreated(User $user)
{
\Auth::login($user);
flash(trans('auth.welcome', ['name' => $user->name]));
return redirect(route('home'));
}
}
================================================
FILE: app/Http/Controllers/WelcomeController.php
================================================
<?php
namespace App\Http\Controllers;
class WelcomeController extends Controller
{
/**
* Constructor.
*/
public function __construct()
{
$this->middleware('auth', ['only' => ['home']]);
parent::__construct();
}
/**
* Get the index page
*
* @return \Illuminate\Contracts\View\Factory
*/
public function index()
{
return view('home');
}
/**
* Get the home page
*
* @return \Illuminate\Contracts\View\Factory
*/
public function home()
{
return view('home');
}
/**
* Set locale preference of a user
*
* @return \Illuminate\Http\RedirectResponse
*/
public function locale()
{
$cookie = cookie()->forever('locale__myProject', request('locale'));
cookie()->queue($cookie);
return ($return = request('return'))
? redirect(urldecode($return))->withCookie($cookie)
: redirect(\Auth::check() ? route('home') : route('index'))->withCookie($cookie);
}
}
================================================
FILE: app/Http/Kernel.php
================================================
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* @var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
// Laravel 5.2 Global middlewares, but will not be used for this project
// 'web' => [
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
// ],
//
// 'api' => [
// 'throttle:60,1',
// ],
];
/**
* The application's route middleware.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'throttle.api'=> \App\Http\Middleware\ThrottleApiRequests::class,
'role' => \Bican\Roles\Middleware\VerifyRole::class,
'author' => \App\Http\Middleware\AuthorOnly::class,
'jwt.auth' => \App\Http\Middleware\GetUserFromToken::class,
'jwt.refresh' => \App\Http\Middleware\RefreshToken::class,
'obfuscate' => \App\Http\Middleware\ObfuscateId::class,
];
}
================================================
FILE: app/Http/Middleware/Authenticate.php
================================================
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return redirect()->guest(
route('sessions.create', ['return' => $request->fullUrl()])
);
}
}
return $next($request);
}
}
================================================
FILE: app/Http/Middleware/AuthorOnly.php
================================================
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AuthorOnly
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $param
* @return mixed
*/
public function handle(Request $request, Closure $next, $param = null)
{
$user = $request->user();
$model = '\\App\\' . ucfirst($param);
$modelId = $request->route($param ? str_plural($param) : 'id');
if (! $model::whereId($modelId)->whereAuthorId($user->id)->exists() and ! $user->isAdmin()) {
if (is_api_request()) {
return json()->forbiddenError();
}
flash()->error(trans('errors.forbidden') . ' : ' . trans('errors.forbidden_description'));
return back();
}
return $next($request);
}
}
================================================
FILE: app/Http/Middleware/EncryptCookies.php
================================================
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
class EncryptCookies extends BaseEncrypter
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}
================================================
FILE: app/Http/Middleware/GetUserFromToken.php
================================================
<?php
namespace App\Http\Middleware;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Middleware\BaseMiddleware;
class GetUserFromToken extends BaseMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
* @throws \Exception
*/
public function handle($request, \Closure $next)
{
if (! $token = $this->auth->setRequest($request)->getToken()) {
throw new JWTException('token_not_provided', 400);
}
if (! $user = $this->auth->authenticate($token)) {
throw new JWTException('user_not_found', 404);
}
$this->events->fire('tymon.jwt.valid', $user);
return $next($request);
}
}
================================================
FILE: app/Http/Middleware/ObfuscateId.php
================================================
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ObfuscateId
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $param
* @return mixed
*/
public function handle(Request $request, Closure $next, $param = null)
{
$routeParamName = $param ? str_plural($param) : 'id';
if ($routeParamValue = $request->route()->getParameter($routeParamName)) {
$request->route()->setParameter($routeParamName, optimus()->decode($routeParamValue));
}
return $next($request);
}
}
================================================
FILE: app/Http/Middleware/RedirectIfAuthenticated.php
================================================
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class RedirectIfAuthenticated
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->check()) {
return redirect('/home');
}
return $next($request);
}
}
================================================
FILE: app/Http/Middleware/RefreshToken.php
================================================
<?php
namespace App\Http\Middleware;
use Tymon\JWTAuth\Middleware\BaseMiddleware;
class RefreshToken extends BaseMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, \Closure $next)
{
$newToken = $this->auth->setRequest($request)->parseToken()->refresh();
return response()->json([
'code' => 201,
'message' => 'success',
'token' => $newToken,
], 201, [
'Authorization' => "Bearer {$newToken}",
]);
}
}
================================================
FILE: app/Http/Middleware/ThrottleApiRequests.php
================================================
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottleApiRequests extends ThrottleRequests
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param int $maxAttempts
* @param int $decayMinutes
* @return mixed
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
return json()->setHeaders([
'Retry-After' => $this->limiter->availableIn($key),
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => 0,
])->tooManyRequestsError();
}
$this->limiter->hit($key, $decayMinutes);
$response = $next($request);
$response->headers->add([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $maxAttempts - $this->limiter->attempts($key) + 1,
]);
return $response;
}
}
================================================
FILE: app/Http/Middleware/VerifyCsrfToken.php
================================================
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [];
/**
* {@inheritDoc}
*/
public function handle($request, Closure $next)
{
if (is_api_request()) {
return $next($request);
}
return parent::handle($request, $next);
}
}
================================================
FILE: app/Http/Requests/ArticlesRequest.php
================================================
<?php
namespace App\Http\Requests;
class ArticlesRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = [];
if ($this->isDelete()) {
$rules = [];
} elseif ($this->isUpdate()) {
$rules = ['tags' => ['array']];
} else {
$rules = [
'title' => 'required',
'content' => 'required',
'tags' => 'required|array'
];
}
return $rules;
}
}
================================================
FILE: app/Http/Requests/FilterArticlesRequest.php
================================================
<?php
namespace App\Http\Requests;
class FilterArticlesRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$params = config('project.params');
$filters = implode(',', array_keys(config('project.filters.article')));
return [
$params['filter'] => "in:{$filters}", // Query scope filter
$params['limit'] => 'size:1,10', // PerPage
$params['sort'] => 'in:created_at,view_count,created', // Sort: Age(created_at), View(view_count)
$params['order'] => 'in:asc,desc', // Direction: Ascending or Descending
$params['search'] => 'alpha_dash', // Search query
$params['page'] => '', // Page number
];
}
}
================================================
FILE: app/Http/Requests/Request.php
================================================
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
abstract class Request extends FormRequest
{
/**
* Determine if the request is update
*
* @return bool
*/
protected function isUpdate()
{
$needle = ['put', 'patch'];
return in_array(strtolower($this->input('_method')), $needle)
or in_array(strtolower($this->header('x-http-method-override')), $needle)
or in_array(strtolower($this->method()), $needle);
}
/**
* Determine if the request is delete
*
* @return bool
*/
protected function isDelete()
{
$needle = ['delete'];
return in_array(strtolower($this->input('_method')), $needle)
or in_array(strtolower($this->header('x-http-method-override')), $needle)
or in_array(strtolower($this->method()), $needle);
}
/**
* {@inheritDoc}
*/
public function response(array $errors)
{
if (is_api_request()) {
return json()->unprocessableError($errors);
}
if ($this->ajax() || $this->wantsJson()) {
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
/**
* {@inheritDoc}
*/
public function forbiddenResponse()
{
if (is_api_request()) {
return json()->forbiddenError();
}
return response('Forbidden', 403);
}
}
================================================
FILE: app/Http/routes.php
================================================
<?php
Route::group(['domain' => config('project.api_domain'), 'as' => 'api.', 'namespace' => 'Api', 'middleware' => 'cors'], function() {
/* Landing page */
Route::get('/', [
'as' => 'index',
'uses' => 'WelcomeController@index'
]);
/* User Registration */
Route::post('auth/register', [
'as' => 'users.store',
'uses' => 'UsersController@store'
]);
/* Session.
* In API, logout path is not required. Because,
* when token is expired, any API request will not be validated.
*/
Route::post('auth/login', [
'as' => 'sessions.store',
'uses' => 'SessionsController@store'
]);
Route::post('auth/refresh', [
'as' => 'sessions.refresh',
'uses' => 'SessionsController@refresh'
]);
/* Social Login
* In API, social login is not provided.
* Each client has to integrate an Oauth library, and
* call 'POST auth/register' route in a onOauthLoginSuccessCallback.
*/
/* Password Reminder.
* Password reset is possible only through the web page.
* For api client, this endpoint will accept user's email address
* and send the user email which contains password reset token.
*/
Route::post('auth/remind', [
'as' => 'remind.store',
'uses' => 'PasswordsController@postRemind',
]);
/* User */
Route::resource('users', 'UsersController', ['only' => ['show']]);
/* api.v1 */
Route::group(['prefix' => 'v1', 'namespace' => 'V1'], function() {
/* Landing page */
Route::get('/', [
'as' => 'v1.index',
'uses' => 'WelcomeController@index'
]);
/* Forum */
Route::get('tags/{slug}/articles', [
'as' => 'v1.tags.articles.index',
'uses' => 'ArticlesController@index'
]);
Route::resource('articles', 'ArticlesController', ['except' => ['create', 'edit']]);
Route::resource('comments', 'CommentsController', ['except' => ['create', 'edit']]);
});
});
Route::group(['domain' => config('project.app_domain')], function() {
/* Landing page */
Route::get('/', [
'as' => 'index',
'uses' => 'WelcomeController@index',
]);
Route::get('home', [
'as' => 'home',
'uses' => 'WelcomeController@home',
]);
Route::get('locale', [
'as' => 'locale',
'uses' => 'WelcomeController@locale',
]);
/* Mailing list */
//Route::post('mail-list/subscribe', [
// 'as' => 'mail-list.subscribe',
// 'uses' => 'MailListController@subscribe',
//]);
//
//Route::delete('mail-list/unsubscribe', [
// 'as' => 'mail-list.unsubscribe',
// 'uses' => 'MailListController@unsubscribe',
//]);
/* Documents */
Route::get('lessons/{image}', [
'as' => 'lessons.image',
'uses' => 'LessonsController@image',
]);
Route::get('lessons/{file?}', [
'as' => 'lessons.show',
'uses' => 'LessonsController@show',
]);
/* Forum */
Route::get('tags/{slug}/articles', [
'as' => 'tags.articles.index',
'uses' => 'ArticlesController@index'
]);
Route::put('articles/{articles}/pick', [
'as' => 'articles.pick-best-comment',
'uses' => 'ArticlesController@pickBest'
]);
Route::resource('articles', 'ArticlesController');
/* Attachments */
Route::resource('files', 'AttachmentsController', ['only' => ['store', 'destroy']]);
/* Comments */
Route::post('comments/{id}/vote', 'CommentsController@vote');
Route::resource('comments', 'CommentsController', ['only' => ['store', 'update', 'destroy']]);
/* User Registration */
Route::get('auth/register', [
'as' => 'users.create',
'uses' => 'UsersController@create'
]);
Route::post('auth/register', [
'as' => 'users.store',
'uses' => 'UsersController@store'
]);
/* Social Login */
Route::get('social/{provider}', [
'as' => 'social.login',
'uses' => 'SocialController@execute',
]);
/* Session */
Route::get('auth/login', [
'as' => 'sessions.create',
'uses' => 'SessionsController@create'
]);
Route::post('auth/login', [
'as' => 'sessions.store',
'uses' => 'SessionsController@store'
]);
Route::get('auth/logout', [
'as' => 'sessions.destroy',
'uses' => 'SessionsController@destroy'
]);
/* Password Reminder */
Route::get('auth/remind', [
'as' => 'remind.create',
'uses' => 'PasswordsController@getRemind',
]);
Route::post('auth/remind', [
'as' => 'remind.store',
'uses' => 'PasswordsController@postRemind',
]);
Route::get('auth/reset/{token}', [
'as' => 'reset.create',
'uses' => 'PasswordsController@getReset',
]);
Route::post('auth/reset', [
'as' => 'reset.store',
'uses' => 'PasswordsController@postReset',
]);
});
/* From Laravel 5.2 all built-in events are fired in the form of Object */
//DB::listen(function($event){
// var_dump($event->sql/*, $event->bindings, $event->time*/);
//});
================================================
FILE: app/Jobs/Job.php
================================================
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "onQueue" and "delay" queue helper methods.
|
*/
use Queueable;
}
================================================
FILE: app/Lesson.php
================================================
<?php
namespace App;
class Lesson extends Model
{
use AuthorTrait;
// Directory name that holds markdown files
// that is corresponding to this Eloquent model.
// This should be relative to project root. e.g. docs/version1
public static $path = 'lessons';
// List of files that should not be included
// when generating index of files.
public static $excepts = [
'INDEX.md',
'SUMMARY.md',
'GLOSSARY.md',
// 'README.md',
];
protected $fillable = [
'author_id',
'name',
'content',
];
/* Relationships */
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
================================================
FILE: app/Listeners/.gitkeep
================================================
================================================
FILE: app/Listeners/CacheHandler.php
================================================
<?php
namespace App\Listeners;
use App\Events\ModelChanged;
class CacheHandler
{
/**
* Handle the event.
*
* @param \App\Events\ModelChanged $event
*/
public function handle(ModelChanged $event)
{
if (! taggable()) {
// Remove all cache store
return \Cache::flush();
}
// Remove only cache that has the given tag(s)
return \Cache::tags($event->cacheTags)->flush();
}
}
================================================
FILE: app/Listeners/CommentsHandler.php
================================================
<?php
namespace App\Listeners;
use App\Comment;
class CommentsHandler
{
protected $to = [];
/**
* Handle the event.
*
* @param \App\Comment $comment
*/
public function handle(Comment $comment)
{
if ($comment->commentable->notification) {
// get the Article author's email and append to the recipients array.
$this->to[] = $comment->commentable->author->email;
}
// get email address lists from the comments and append to the recipients array.
$this->findEmail($comment);
// Remove duplicate email address.
$to = array_unique($this->to);
$subject = 'New comment';
return \Mail::send('emails.new-comment', compact('comment'), function($m) use($to, $subject) {
return $m->to($to)->subject($subject);
});
}
/**
* Recursively find email address from the comments and push them to recipients list.
*
* @param \App\Comment $comment
*/
protected function findEmail(Comment $comment)
{
if ($comment->parent) {
$this->to[] = $comment->parent->author->email;
return $this->findEmail($comment->parent);
}
return;
}
}
================================================
FILE: app/Listeners/UserEventsHandler.php
================================================
<?php
namespace App\Listeners;
use App\User;
use Illuminate\Contracts\Events\Dispatcher;
class UserEventsHandler
{
/**
* Map events and handlers
*
* @param Dispatcher $events
*/
public function subscribe(Dispatcher $events) {
$events->listen(
'users.login',
__CLASS__ . '@onUserLogin'
);
}
/**
* User login event handler
*
* @param \App\User $user
*/
public function onUserLogin(User $user)
{
// Update last_login field
$user->last_login = \Carbon\Carbon::now()->toDateTimeString();
$user->save();
}
}
================================================
FILE: app/Listeners/ViewCountHandler.php
================================================
<?php
namespace App\Listeners;
use App\Events\ArticleConsumed;
class ViewCountHandler
{
/**
* Handle the event.
*
* @param ArticleConsumed $event
* @return void
*/
public function handle(ArticleConsumed $event)
{
// Increase view count
$event->article->view_count ++;
$event->article->save();
}
}
================================================
FILE: app/Model.php
================================================
<?php
namespace App;
use Illuminate\Database\Eloquent\Model as Eloquent;
abstract class Model extends Eloquent
{
}
================================================
FILE: app/Policies/.gitkeep
================================================
================================================
FILE: app/Providers/AppServiceProvider.php
================================================
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
if ($locale = request()->cookie('locale__myProject')) {
app()->setLocale(\Crypt::decrypt($locale));
}
\Carbon\Carbon::setLocale(app()->getLocale());
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(\Jenssegers\Optimus\Optimus::class, function () {
return new \Jenssegers\Optimus\Optimus(132961291, 1484265379, 37817169);
});
if ($this->app->environment('local')) {
$this->app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
}
}
}
================================================
FILE: app/Providers/AuthServiceProvider.php
================================================
<?php
namespace App\Providers;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any application authentication / authorization services.
*
* @param \Illuminate\Contracts\Auth\Access\Gate $gate
* @return void
*/
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
//
}
}
================================================
FILE: app/Providers/EventServiceProvider.php
================================================
<?php
namespace App\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
\App\Events\ModelChanged::class => [
\App\Listeners\CacheHandler::class
],
\App\Events\ArticleConsumed::class => [
\App\Listeners\ViewCountHandler::class
]
];
/**
* Register any other events for your application.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function boot(DispatcherContract $events)
{
parent::boot($events);
$events->listen('comments.*', \App\Listeners\CommentsHandler::class);
$events->subscribe(\App\Listeners\UserEventsHandler::class);
}
}
================================================
FILE: app/Providers/RouteServiceProvider.php
================================================
<?php
namespace App\Providers;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to the controller routes in your routes file.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
protected $namespace = 'App\Http\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function boot(Router $router)
{
$router->pattern('id', '[0-9]+');
// Should handle 32n33-sss-img-dd.png exception
$router->pattern('image', 'images/(?P<parent>[0-9n]{2,5}-[\pL-\pN\._-]+)-(?P<suffix>img-[0-9]{2}.png)');
parent::boot($router);
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function map(Router $router)
{
$router->group(['namespace' => $this->namespace], function ($router) {
require app_path('Http/routes.php');
});
}
}
================================================
FILE: app/Reporters/ErrorReport.php
================================================
<?php
namespace App\Reporters;
use Carbon\Carbon;
use Maknz\Slack\Attachment;
use Maknz\Slack\AttachmentField;
use Maknz\Slack\Client;
use Request;
use Route;
class ErrorReport
{
/**
* @var \Maknz\Slack\Client
*/
private $client;
/**
* @var \Exception
*/
private $primitive;
/**
* @param \Exception $e
* @param string $webhook
* @param array $settings
*/
public function __construct(\Exception $e, $webhook = '', $settings = [])
{
$this->primitive = $e;
$webhook = $webhook ?: env('SLACK_WEBHOOK');
$this->client = $this->createClient($webhook, $settings);
}
/**
* Send slack report.
*
* @return mixed
*/
public function send()
{
return $this->client->createMessage()->attach($this->buildPayload())->send();
}
/**
* Build Slack Attachment array based on given Exception object.
*
* @see https://api.slack.com/docs/attachments
*
* @return array
*/
protected function buildPayload()
{
return new Attachment([
'fallback' => 'Error Report',
'text' => $this->primitive->getMessage() ?: "Something broken :(",
'color' => 'danger',
'fields' => [
new AttachmentField([
'title' => 'localtime',
'value' => Carbon::now('Asia/Seoul')->toDateTimeString(),
]),
new AttachmentField([
'title' => 'username',
'value' => (auth()->check() ? auth()->user()->email : 'Unknown')
. sprintf(' (%s)', Request::ip()),
]),
new AttachmentField([
'title' => 'route',
'value' => Route::currentRouteName()
. sprintf(
' (%s %s)',
Request::method(),
Request::fullUrl()
),
]),
new AttachmentField([
'title' => 'description',
'value' => sprintf(
'%s in %s line %d',
get_class($this->primitive),
pathinfo($this->primitive->getFile(), PATHINFO_BASENAME),
$this->primitive->getLine()
),
]),
new AttachmentField([
'title' => 'trace',
'value' => $this->primitive->getTraceAsString(),
]),
],
]);
}
/**
* Factory - Create HTTP API Client for Slack
*
* @param array $overrides
* @return \Maknz\Slack\Client
*/
protected function createClient($webhook, $overrides = [])
{
$settings = array_merge([
'channel' => '#l5essential',
'username' => 'aws-demo',
'link_names' => true,
'unfurl_links' => true,
'markdown_in_attachments' => ['title', 'text', 'fields'],
], $overrides);
return new Client($webhook, $settings);
}
}
================================================
FILE: app/Reporters/MonologSlackReport.php
================================================
<?php
namespace App\Reporters;
class MonologSlackReport
{
/**
* @var \Monolog\Logger
*/
protected $logger;
public function __construct()
{
$this->createLogger();
}
/**
* Send slack report.
*
* @param \Exception $e
* @return mixed
*/
public function send(\Exception $e)
{
return $this->logger->error(
$e->getMessage() ?: "Something broken :(",
$this->buildPayload($e)
);
}
/**
* Build payload array based on given Exception object.
*
* @param \Exception $e
* @return array
*/
protected function buildPayload(\Exception $e)
{
return [
'username' => auth()->check() ? auth()->user()->email : 'Unknown',
'route' => \Route::currentRouteName(),
'localtime' => \Carbon\Carbon::now('Asia/Seoul')->toDateTimeString(),
'exception' => [
'class' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
'code' => $e->getCode(),
'trace' => $e->getTraceAsString(),
'ip' => \Request::ip(),
'method' => \Request::method(),
'url' => \Request::fullUrl(),
'content' => \Request::instance()->getContent()
?: json_encode(\Request::all()),
'headers' => \Request::header(),
],
];
}
/**
* Factory - Create Monolog instance which has a Slack handler.
*/
protected function createLogger()
{
$logger = \Log::getMonolog();
$logger->pushHandler(
(new \Monolog\Handler\SlackHandler(env('SLACK_TOKEN'), '#l5essential', 'aws-demo'))
->setLevel(\Monolog\Logger::ERROR)
);
return $this->logger = $logger;
}
}
================================================
FILE: app/Repositories/LessonRepository.php
================================================
<?php
namespace App\Repositories;
class LessonRepository extends MarkdownRepository
{
/**
* Get the index of documents.
*
* @return mixed
*/
public function index()
{
return $this->find('INDEX.md');
}
/**
* Specify an Eloquent Model's class name.
*
* @return string
*/
public function model()
{
return \App\Lesson::class;
}
}
================================================
FILE: app/Repositories/MarkdownRepository.php
================================================
<?php
namespace App\Repositories;
use Cache;
use Exception;
use File;
use Illuminate\Database\Eloquent\Model;
use Image;
abstract class MarkdownRepository implements RepositoryInterface
{
/**
* @var \Illuminate\Database\Eloquent\Model
*/
protected $model;
/**
* @var string Directory name, that houses the markdown files.
*/
protected $path;
/**
* @var array Table of markdown files.
*/
protected $toc;
/**
* @var string Currently selected markdown file.
*/
protected $current;
/**
* Create DocumentRepository instance.
*/
public function __construct()
{
$this->initialize();
}
/**
* Specify an Eloquent Model's class name.
*
* @return string
*/
public abstract function model();
/**
* Factory - new up the model and set the required properties.
*
* @return \Illuminate\Database\Eloquent\Model
* @throws \Exception
*/
protected function initialize()
{
$model = app()->make($this->model());
if (! $model instanceof Model) {
throw new Exception(
'model() method must return a string name of an Eloquent Model. Or the provided model cannot be instantiable.'
);
}
if (! property_exists($this->model(), 'path')) {
throw new Exception(
"{$this->model()} should have a property named 'path'"
);
}
$path = base_path($model::$path);
if (! File::isDirectory($path)) {
throw new Exception(
"Something went wrong with the path property of {$this->model()} model."
);
}
$this->model = $model;
$this->path = $path;
if (! $this->toc) {
// Todo Expensive job. Should apply cache..
$this->toc = Cache::remember('lessons.index', 120, function() use($model) {
$all = glob(base_path($model::$path . DIRECTORY_SEPARATOR . '*.md'));
$excepts = [];
if (property_exists($this->model(), 'excepts')) {
foreach ($model::$excepts as $except) {
$excepts[] = base_path($model::$path . DIRECTORY_SEPARATOR . $except);
}
}
$files = array_diff($all, $excepts);
return array_map(function($file) {
return pathinfo($file, PATHINFO_BASENAME);
}, $files);
});
}
return;
}
/**
* Get the collection of model.
*
* @param array $columns
* @return \Illuminate\Database\Eloquent\Collection
*/
public function all($columns = ['*'])
{
return $this->model->get($columns);
}
/**
* Get the table of contents.
*
* @return array
*/
public function toc()
{
return $this->toc;
}
/**
* Get the currently selected markdown's filename.
*
* @return string
*/
public function current()
{
return $this->current;
}
/**
* Calculate previous entry.
*
* @param $current
* @return bool
*/
public function prev($current) {
$prev = array_search($current, $this->toc) - 1;
return array_key_exists($prev, $this->toc)
? $this->toc[$prev]
: false;
}
/**
* Calculate next entry.
*
* @param $current
* @return mixed
*/
public function next($current) {
$next = array_search($current, $this->toc) + 1;
return array_key_exists($next, $this->toc)
? $this->toc[$next]
: false;
}
/**
* Get the model instance.
*
* @param mixed $id filename
* @param array $columns
* @return \Illuminate\Database\Eloquent\Model
*/
public function find($id, $columns = ['*'])
{
$this->current = $id;
return $this->model->whereName($id)->first()
?: $this->model->create([
'author_id' => 1, // Bad!! Avoid hard code, b.c, admin may change.
'name' => $id,
'content' => File::get($this->getPath($id)),
]);
}
/**
* Calculate and respond image path.
*
* @param string $file
* @return \Intervention\Image\Image
*/
public function image($file)
{
return Image::make($this->getPath($file));
}
/**
* Create etag value
*
* @param string $file
* @return string
*/
public function etag($file)
{
return md5($file . '/' . File::lastModified($this->getPath($file)));
}
/**
* Calculate full path to the given file.
*
* @param string $file
* @return string
*/
protected function getPath($file)
{
$path = $this->path . DIRECTORY_SEPARATOR . $file;
if (!File::exists($path)) {
abort(404, 'File not exist');
}
return $path;
}
}
================================================
FILE: app/Repositories/RepositoryInterface.php
================================================
<?php
namespace App\Repositories;
interface RepositoryInterface
{
/**
* Get the model instance.
*
* @param mixed $id
* @param array $columns
* @return \Illuminate\Database\Eloquent\Model
*/
public function find($id, $columns = ['*']);
/**
* Get the collection of model.
*
* @param array $columns
* @return \Illuminate\Database\Eloquent\Collection
*/
public function all($columns = ['*']);
}
================================================
FILE: app/Services/Markdown.php
================================================
<?php
namespace App\Services;
use ParsedownExtra;
class Markdown extends ParsedownExtra {
// Pattern to search for 'article#000, ArTicle#00, A#0, a#00, ...' mention
const PATTERN_ARTICLE = '/(article|a)(\#|\@|\:\:)(?P<id>\d+)/i';
const PATTERN_REMOVE = '/<!--\s?@start\s?-->[\w\W\d\D]+<!--\s?@end\s?-->/';
const PATTERN_FRONT_FORMATTER = '/---[\w\W\d\D]+extends[\w\W\d\D]+---/';
/**
* Add link to another articles
*
* @param $text
* @return mixed|string
*/
public function text($text) {
if (preg_match(self::PATTERN_ARTICLE, $text, $matches) > 0) {
$text = preg_replace_callback(self::PATTERN_ARTICLE, function ($matches) {
return sprintf(
"<a href='%s'>%s</a>",
route('articles.show', $matches['id']),
$matches[0]
);
}, $text);
}
if (preg_match(self::PATTERN_REMOVE, $text, $matches) > 0) {
$text = preg_replace_callback(self::PATTERN_REMOVE, function ($matches) {
return '';
}, $text);
}
if (preg_match(self::PATTERN_FRONT_FORMATTER, $text, $matches) > 0) {
$text = preg_replace_callback(self::PATTERN_FRONT_FORMATTER, function ($matches) {
return '';
}, $text);
}
return parent::text($text);
}
}
================================================
FILE: app/Tag.php
================================================
<?php
namespace App;
class Tag extends Model
{
protected $fillable = [
'name',
'slug'
];
/* Relationships */
public function articles()
{
return $this->belongsToMany(Article::class)->withTimestamps();
}
}
================================================
FILE: app/Transformers/ArticleTransformer.php
================================================
<?php
namespace App\Transformers;
use App\Article;
use Appkr\Api\TransformerAbstract;
use League\Fractal\ParamBag;
class ArticleTransformer extends TransformerAbstract
{
/**
* List of resources possible to include using url query string.
* e.g. collection case -> ?include=comments:limit(5|1):order(created_at|desc)
* item case -> ?include=author
*
* @var array
*/
protected $availableIncludes = ['comments', 'author', 'tags', 'attachments'];
/**
* Transform single resource.
*
* @param \App\Article $article
* @return array
*/
public function transform(Article $article)
{
$id = optimus((int) $article->id);
$payload = [
'id' => $id,
'title' => $article->title,
'content_raw' => strip_tags($article->content),
'content_html' => markdown($article->content),
'created' => $article->created_at->toIso8601String(),
'view_count' => (int) $article->view_count,
'link' => [
'rel' => 'self',
'href' => route('api.v1.articles.show', $id),
],
'comments' => (int) $article->comments->count(),
'author' => [
'name' => $article->author->name,
'email' => $article->author->email,
'avatar' => 'http:' . gravatar_profile_url($article->author->email),
],
'tags' => $article->tags->pluck('slug'),
'attachments' => (int) $article->attachments->count(),
];
if ($fields = $this->getPartialFields()) {
$payload = array_only($payload, $fields);
}
return $payload;
}
/**
* Include comments.
*
* @param \App\Article $article
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Collection
* @throws \Exception
*/
public function includeComments(Article $article, ParamBag $params = null)
{
$transformer = new \App\Transformers\CommentTransformer($params);
$parsed = $this->getParsedParams();
$comments = $article->comments()->limit($parsed['limit'])->offset($parsed['offset'])->orderBy($parsed['sort'], $parsed['order'])->get();
return $this->collection($comments, $transformer);
}
/**
* Include author.
*
* @param \App\Article $article
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Item
*/
public function includeAuthor(Article $article, ParamBag $params = null)
{
return $this->item($article->author, new \App\Transformers\UserTransformer($params));
}
/**
* Include tags.
*
* @param \App\Article $article
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Collection
* @throws \Exception
*/
public function includeTags(Article $article, ParamBag $params = null)
{
$transformer = new \App\Transformers\TagTransformer($params);
$parsed = $this->getParsedParams();
$tags = $article->tags()->limit($parsed['limit'])->offset($parsed['offset'])->orderBy($parsed['sort'], $parsed['order'])->get();
return $this->collection($tags, $transformer);
}
/**
* Include attachments.
*
* @param \App\Article $article
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Collection
* @throws \Exception
*/
public function includeAttachments(Article $article, ParamBag $params = null)
{
$transformer = new \App\Transformers\AttachmentTransformer($params);
$parsed = $this->getParsedParams();
$attachments = $article->attachments()->limit($parsed['limit'])->offset($parsed['offset'])->orderBy($parsed['sort'], $parsed['order'])->get();
return $this->collection($attachments, $transformer);
}
}
================================================
FILE: app/Transformers/AttachmentTransformer.php
================================================
<?php
namespace App\Transformers;
use App\Attachment;
use Appkr\Api\TransformerAbstract;
use League\Fractal\ParamBag;
class AttachmentTransformer extends TransformerAbstract
{
/**
* Transform single resource.
*
* @param \App\Attachment $attachment
* @return array
*/
public function transform(Attachment $attachment)
{
$payload = [
'id' => optimus((int) $attachment->id),
'name' => $attachment->name,
'created' => $attachment->created_at->toIso8601String(),
'link' => [
'rel' => 'self',
'href' => url(sprintf('http://%s:8000/attachments/%s', config('project.app_domain'), $attachment->name)),
],
];
if ($fields = $this->getPartialFields()) {
$payload = array_only($payload, $fields);
}
return $payload;
}
}
================================================
FILE: app/Transformers/CommentTransformer.php
================================================
<?php
namespace App\Transformers;
use App\Comment;
use Appkr\Api\TransformerAbstract;
;
use League\Fractal\ParamBag;
class CommentTransformer extends TransformerAbstract
{
/**
* List of resources possible to include using url query string.
* e.g. collection case -> ?include=comments:limit(5|1):order(created_at|desc)
* item case -> ?include=author
*
* @var array
*/
protected $availableIncludes = ['author'];
/**
* Transform single resource.
*
* @param \App\Comment $comment
* @return array
*/
public function transform(Comment $comment)
{
$id = optimus((int) $comment->id);
$payload = [
'id' => $id,
'content_raw' => strip_tags($comment->content),
'content_html' => markdown($comment->content),
'created' => $comment->created_at->toIso8601String(),
'vote' => ['up' => (int) $comment->up_count, 'down' => (int) $comment->down_count],
'link' => [
'rel' => 'self',
'href' => route('api.v1.comments.show', $id),
],
'author' => [
'name' => $comment->author->name,
'email' => $comment->author->email,
'avatar' => 'http:' . gravatar_profile_url($comment->author->email),
],
];
if ($fields = $this->getPartialFields()) {
$payload = array_only($payload, $fields);
}
return $payload;
}
/**
* Include author.
*
* @param \App\Comment $comment
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Item
*/
public function includeAuthor(Comment $comment, ParamBag $params = null)
{
return $this->item($comment->author, new \App\Transformers\UserTransformer($params));
}
}
================================================
FILE: app/Transformers/TagTransformer.php
================================================
<?php
namespace App\Transformers;
use App\Tag;
use Appkr\Api\TransformerAbstract;
use League\Fractal\ParamBag;
class TagTransformer extends TransformerAbstract
{
protected $availableIncludes = ['articles'];
/**
* Transform single resource.
*
* @param \App\Tag $tag
* @return array
*/
public function transform(Tag $tag)
{
$payload = [
'id' => optimus((int) $tag->id),
'slug' => $tag->slug,
'created' => $tag->created_at->toIso8601String(),
'link' => [
'rel' => 'self',
'href' => route('api.v1.tags.articles.index', $tag->slug),
],
'articles' => (int) $tag->articles->count(),
];
if ($fields = $this->getPartialFields()) {
$payload = array_only($payload, $fields);
}
return $payload;
}
/**
* Include articles.
*
* @param \App\Tag $tag
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Collection
* @throws \Exception
*/
public function includeArticles(Tag $tag, ParamBag $params = null)
{
$transformer = new \App\Transformers\ArticleTransformer($params);
$parsed = $this->getParsedParams();
$articles = $tag->articles()->limit($parsed['limit'])->offset($parsed['offset'])->orderBy($parsed['sort'], $parsed['order'])->get();
return $this->collection($articles, $transformer);
}
}
================================================
FILE: app/Transformers/UserTransformer.php
================================================
<?php
namespace App\Transformers;
use App\User;
use Appkr\Api\TransformerAbstract;
use League\Fractal\ParamBag;
class UserTransformer extends TransformerAbstract
{
/**
* List of resources possible to include using url query string.
* e.g. collection case -> ?include=comments:limit(5|1):order(created_at|desc)
* item case -> ?include=author
*
* @var array
*/
protected $availableIncludes = ['articles', 'comments'];
/**
* Transform single resource.
*
* @param \App\User $user
* @return array
*/
public function transform(User $user)
{
$id = optimus((int) $user->id);
$payload = [
'id' => $id,
'name' => $user->name,
'email' => $user->email,
'avatar' => 'http:' . gravatar_profile_url($user->email),
'signup' => $user->created_at->toIso8601String(),
'link' => [
'rel' => 'self',
'href' => route('api.users.show', $id),
],
'articles' => (int) $user->articles->count(),
'comments' => (int) $user->comments->count(),
];
if ($fields = $this->getPartialFields()) {
$payload = array_only($payload, $fields);
}
return $payload;
}
/**
* Include articles.
*
* @param \App\User $user
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Collection
* @throws \Exception
*/
public function includeArticles(User $user, ParamBag $params = null)
{
$transformer = new \App\Transformers\ArticleTransformer($params);
$parsed = $this->getParsedParams();
$articles = $user->articles()->limit($parsed['limit'])->offset($parsed['offset'])->orderBy($parsed['sort'], $parsed['order'])->get();
return $this->collection($articles, $transformer);
}
/**
* Include comments.
*
* @param \App\User $user
* @param \League\Fractal\ParamBag|null $params
* @return \League\Fractal\Resource\Collection
* @throws \Exception
*/
public function includeComments(User $user, ParamBag $params = null)
{
$transformer = new \App\Transformers\CommentTransformer($params);
$parsed = $this->getParsedParams();
$comments = $user->comments()->limit($parsed['limit'])->offset($parsed['offset'])->orderBy($parsed['sort'], $parsed['order'])->get();
return $this->collection($comments, $transformer);
}
}
================================================
FILE: app/User.php
================================================
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Bican\Roles\Traits\HasRoleAndPermission;
use Bican\Roles\Contracts\HasRoleAndPermission as HasRoleAndPermissionContract;
class User extends Model implements AuthenticatableContract,
CanResetPasswordContract,
HasRoleAndPermissionContract
{
use Authenticatable;
use CanResetPassword;
use HasRoleAndPermission;
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'email', 'password'];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token'];
protected $dates = ['last_login'];
/* Relationships */
public function articles()
{
return $this->hasMany(Article::class, 'author_id');
}
public function comments()
{
return $this->hasMany(Comment::class, 'author_id');
}
public function votes()
{
return $this->hasMany(Vote::class);
}
/* Query Scopes */
public function scopeNoPassword($query)
{
return $query->whereNull('password');
}
/* Helpers */
public function isAdmin()
{
return $this->roles()->whereSlug('admin')->exists();
}
}
================================================
FILE: app/Vote.php
================================================
<?php
namespace App;
class Vote extends Model
{
public $timestamps = false;
protected $fillable = [
'user_id',
'comment_id',
'up',
'down',
'voted_at',
];
protected $dates = [
'voted_at',
];
/* Relationships */
public function comment()
{
return $this->belongsTo(Comment::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
================================================
FILE: app/helpers.php
================================================
<?php
if (! function_exists('markdown')) {
/**
* Compile the given text to markdown document.
*
* @param string $text
* @return string
*/
function markdown($text)
{
return app(App\Services\Markdown::class)->text($text);
}
}
if (! function_exists('icon')) {
/**
* Generate FontAwesome icon tag
*
* @param string $class font-awesome class name
* @param string $addition additional class
* @param string $inline inline style
* @return string
*/
function icon($class, $addition = 'icon', $inline = null)
{
$icon = config('icons.' . $class);
$inline = $inline ? 'style="' . $inline . '"' : null;
return sprintf('<i class="%s %s" %s></i>', $icon, $addition, $inline);
}
}
if (! function_exists('attachment_path')) {
/**
* @param string $path
*
* @return string
*/
function attachment_path($path = '')
{
return public_path($path ? 'attachments' . DIRECTORY_SEPARATOR . $path : 'attachments');
}
}
if (! function_exists('gravatar_profile_url')) {
/**
* Get gravatar profile page url
*
* @param string $email
* @return string
*/
function gravatar_profile_url($email)
{
return sprintf("//www.gravatar.com/%s", md5($email));
}
}
if (! function_exists('gravatar_url')) {
/**
* Get gravatar image url
*
* @param string $email
* @param integer $size
* @return string
*/
function gravatar_url($email, $size = 48)
{
return sprintf("//www.gravatar.com/avatar/%s?s=%s", md5($email), $size);
}
}
if (! function_exists('taggable')) {
/**
* Determine if the current cache driver has cacheTags() method
*
* @return bool
*/
function taggable()
{
return ! in_array(config('cache.default'), ['file', 'database']);
}
}
if (! function_exists('link_for_sort')) {
/**
* Build HTML anchor tag for sorting
*
* @param string $column
* @param string $text
* @param array $params
* @return string
*/
function link_for_sort($column, $text, $params = [])
{
$config = config('project.params');
$direction = request()->input($config['order']);
$reverse = ($direction == 'asc') ? 'desc' : 'asc';
if (request()->input($config['sort']) == $column) {
// Update passed $text var, only if it is active sort
$text = sprintf(
"%s %s",
$direction == 'asc' ? icon('asc') : icon('desc'),
$text
);
}
$queryString = http_build_query(array_merge(
request()->except([$config['page'], $config['sort'], $config['order']]),
[$config['sort'] => $column, $config['order'] => $reverse],
$params
));
return sprintf(
'<a href="%s?%s">%s</a>',
urldecode(request()->url()),
$queryString,
$text
);
}
}
if (! function_exists('current_url')) {
/**
* Build current url string, without return param.
*
* @return string
*/
function current_url()
{
if (! request()->has('return')) {
return request()->fullUrl();
}
return sprintf(
'%s?%s',
request()->url(),
http_build_query(request()->except('return'))
);
}
}
if (! function_exists('is_api_request')) {
/**
* Determine if the current request is for HTTP api.
*
* @return bool
*/
function is_api_request()
{
return starts_with(request()->getHttpHost(), config('project.api_domain'));
}
}
if (! function_exists('optimus')) {
/**
* Create Optimus instance.
*
* @param int|null $id
* @return int|\Jenssegers\Optimus\Optimus
*/
function optimus($id = null)
{
$factory = app(\Jenssegers\Optimus\Optimus::class);
if (func_num_args() === 0) {
return $factory;
}
return $factory->encode($id);
}
}
if (! function_exists('cache_key')) {
/**
* Generate key for caching.
*
* Note. Even though the request endpoints are the same,
* the response body should be different because of the query string.
*
* @param $base
* @return string
*/
function cache_key($base)
{
$key = ($uri = request()->fullUrl())
? $base . '.' . urlencode($uri)
: $base;
return md5($key);
}
}
================================================
FILE: artisan
================================================
#!/usr/bin/env php
<?php
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/bootstrap/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running. We will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);
================================================
FILE: bootstrap/app.php
================================================
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;
================================================
FILE: bootstrap/autoload.php
================================================
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Composer Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Include The Compiled Class File
|--------------------------------------------------------------------------
|
| To dramatically increase your application's performance, you may use a
| compiled class file which contains all of the classes commonly used
| by a request. The Artisan "optimize" is used to create this file.
|
*/
$compiledPath = __DIR__.'/cache/compiled.php';
if (file_exists($compiledPath)) {
require $compiledPath;
}
================================================
FILE: bootstrap/cache/.gitignore
================================================
*
!.gitignore
================================================
FILE: bower.json
================================================
{
"name": "myProject",
"devDependencies": {
"bootstrap-sass": "~3.3.5",
"font-awesome": "~4.4.0",
"dropzone": "~4.2.0",
"fastclick": "~1.0.6",
"tabby": "~0.13.1",
"autosize": "~3.0.14",
"marked": "~0.3.5",
"highlightjs": "~8.9.1",
"select2-bootstrap-theme": "~0.1.0-beta.4",
"select2": "~4.0.1"
}
}
================================================
FILE: composer.json
================================================
{
"name": "appkr/l5essential",
"description": "Laravel 5 Essential",
"keywords": [
"laravel",
"how-to",
"lesson",
"beginners"
],
"authors": [
{
"name": "appkr",
"email": "juwonkim@me.com"
}
],
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.2.*",
"erusev/parsedown-extra": " 0.7.*",
"intervention/image": "2.3.*",
"laracasts/flash": "1.3.*",
"laravel/socialite": "2.0.*",
"bican/roles": " 2.1.*",
"maknz/slack": "1.*",
"tymon/jwt-auth": "0.5.*",
"appkr/api": "0.1.*",
"jenssegers/optimus": "^0.2.0",
"asm89/stack-cors": "dev-master as 0.2.2",
"barryvdh/laravel-cors": "^0.7.3"
},
"require-dev": {
"symfony/dom-crawler": "~3.0",
"symfony/css-selector": "~3.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0",
"phpspec/phpspec": "~2.1",
"barryvdh/laravel-ide-helper": "^2.1"
},
"autoload": {
"classmap": [
"database"
],
"files": [
"app/helpers.php"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Test\\": "tests/integration/"
}
},
"scripts": {
"post-install-cmd": [
"php artisan clear-compiled",
"php artisan optimize",
"php artisan cache:clear"
],
"pre-update-cmd": [
"php artisan clear-compiled"
],
"post-update-cmd": [
"php artisan optimize",
"php artisan cache:clear"
],
"post-root-package-install": [
"php -r \"copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"php artisan key:generate"
]
},
"config": {
"preferred-install": "dist"
}
}
================================================
FILE: config/api.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| Debug
|--------------------------------------------------------------------------
|
| If set to true, debug information will be include in api response.
| Must set to false for production.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| API Endpoint pattern
|--------------------------------------------------------------------------
|
| Path 'pattern' used for is_api_request() Helper.
| Provide 'domain', if the api routes are distinguished by domain name.
|
*/
'endpoint' => [
'pattern' => 'v1/*',
'domain' => env('API_DOMAIN', 'api.myproject.dev'),
],
/*
|--------------------------------------------------------------------------
| Include by query string.
|--------------------------------------------------------------------------
|
| If you defined 'availableInclude' property and includeXxx methods
| in a transformer, you can include sub resources using query string.
| e.g. /authors?include=books:limit(3|0):order(id|desc) means
| including 3 records of 'authors', which is reverse ordered by 'id' field,
| without any skipping(0).
|
| An API client can pass list of includes using array or csv string format.
| e.g. /authors?include[]=books:limit(2|0)&include[]=comments:order(id|asc)
| /authors?include=books:limit(2|0),comments:order(id|asc)
|
| For sub-resource inclusion, client can use dot(.) notation.
| e.g. /books?include=author,publisher.somethingelse
|
*/
'include' => [
'key' => 'include',
'params' => [ // available modifier params and their default value
'limit' => [3, 0], // [limit, offset]
'sort' => ['created_at', 'desc'], // [sortKey, sortDirection]
],
],
/*
|--------------------------------------------------------------------------
| Partial response
|--------------------------------------------------------------------------
|
| API clients are allowed to select the response format using query string.
| This will help saving network bandwidth..
| e.g. /author?fields=id,title,link&include=books:fields(id|title|published_at)
|
*/
'partial' => [
'key' => 'fields',
],
/*
|--------------------------------------------------------------------------
| Transformer directory and namespace.
|--------------------------------------------------------------------------
|
| Below config will be applied when we run 'make:transformer' artisan cmd.
| The generated class will be saved at 'dir', and namespaced as you set.
| Note that the 'dir' should be relative to the project root.
|
*/
'transformer' => [
'dir' => 'app/Transformers',
'namespace' => 'App\\Transformers',
],
/*
|--------------------------------------------------------------------------
| Fractal Serializer
|--------------------------------------------------------------------------
|
| Refer to
| http://fractal.thephpleague.com/serializers/
|
*/
'serializer' => \League\Fractal\Serializer\ArraySerializer::class,
/*
|--------------------------------------------------------------------------
| Default Response Headers
|--------------------------------------------------------------------------
|
| Default response headers that every resource/simple response should includes
|
*/
'defaultHeaders' => ['X-Powered-By' => 'appkr/api'],
/*
|--------------------------------------------------------------------------
| Suppress HTTP status code
|--------------------------------------------------------------------------
|
| If set to true, the status code will be fixed to 200.
|
*/
'suppress_response_code' => false,
/*
|--------------------------------------------------------------------------
| Success Response Format
|--------------------------------------------------------------------------
|
| The format will be used at the ApiResponse to respond with success message.
| respondNoContent(), respondSuccess(), respondCreated() consumes this format
|
*/
'successFormat' => [
'success' => [
'code' => ':code',
'message' => ':message',
]
],
/*
|--------------------------------------------------------------------------
| Error Response Format
|--------------------------------------------------------------------------
|
| The format will be used at the ApiResponse to respond with error message.
| respondWithError(), respondForbidden()... consumes this format
|
*/
'errorFormat' => [
'error' => [
'code' => ':code',
'message' => ':message',
]
]
];
================================================
FILE: config/app.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services your application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/
'url' => env('APP_URL', 'http://localhost:8000'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'ko',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY', 'SomeRandomString'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Logging Configuration
|--------------------------------------------------------------------------
|
| Here you may configure the log settings for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Settings: "single", "daily", "syslog", "errorlog"
|
*/
'log' => env('APP_LOG', 'single'),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
// Illuminate\Foundation\Providers\ArtisanServiceProvider::class, // removed while migrating to 5.2
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
// Illuminate\Routing\ControllerServiceProvider::class, // removed while migrating to 5.2
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
/*
* 3rd Party Service Providers
*/
Intervention\Image\ImageServiceProvider::class,
Laracasts\Flash\FlashServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
Bican\Roles\RolesServiceProvider::class,
Maknz\Slack\SlackServiceProvider::class,
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,
Appkr\Api\ApiServiceProvider::class,
Barryvdh\Cors\ServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Input' => Illuminate\Support\Facades\Input::class,
'Inspiring' => Illuminate\Foundation\Inspiring::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
/*
* 3rd Party Facade
*/
'Image' => Intervention\Image\Facades\Image::class,
'Flash' => Laracasts\Flash\Flash::class,
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
'Slack' => Maknz\Slack\Facades\Slack::class,
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
],
];
================================================
FILE: config/auth.php
================================================
<?php
// Copied from https://github.com/laravel/laravel/blob/master/config/auth.php
// while migrating to Laravel 5.2
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| Here you may set the options for resetting passwords including the view
| that is your password reset e-mail. You may also set the name of the
| table that maintains all of the reset tokens for your application.
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'email' => 'emails.password',
'table' => 'password_resets',
'expire' => 60,
],
],
];
================================================
FILE: config/broadcasting.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
*/
'default' => env('BROADCAST_DRIVER', 'pusher'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over websockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_KEY'),
'secret' => env('PUSHER_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
//
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'log' => [
'driver' => 'log',
],
],
];
================================================
FILE: config/cache.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache connection that gets used while
| using this caching library. This connection is used when another is
| not explicitly specified when executing a given caching function.
|
*/
'default' => env('CACHE_DRIVER', 'file'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
*/
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
],
'database' => [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache'),
],
'memcached' => [
'driver' => 'memcached',
'servers' => [
[
'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing a RAM based store such as APC or Memcached, there might
| be other applications utilizing the same cache. So, we'll specify a
| value to get prefixed to all our keys so we can avoid collisions.
|
*/
'prefix' => 'laravel',
];
================================================
FILE: config/compile.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| Additional Compiled Classes
|--------------------------------------------------------------------------
|
| Here you may specify additional classes to include in the compiled file
| generated by the `artisan optimize` command. These should be classes
| that are included on basically every request into the application.
|
*/
'files' => [
//
],
/*
|--------------------------------------------------------------------------
| Compiled File Providers
|--------------------------------------------------------------------------
|
| Here you may list service providers which define a "compiles" function
| that returns additional files that should be compiled, providing an
| easy way to get common files from any packages you are utilizing.
|
*/
'providers' => [
//
],
];
================================================
FILE: config/cors.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| Laravel CORS
|--------------------------------------------------------------------------
|
| allowedOrigins, allowedHeaders and allowedMethods can be set to array('*')
| to accept any value, the allowed methods however have to be explicitly listed.
|
*/
'supportsCredentials' => false,
'allowedOrigins' => ['*'],
'allowedHeaders' => ['*'],
'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE'],
'exposedHeaders' => [],
'maxAge' => 0,
'hosts' => [],
];
================================================
FILE: config/database.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| PDO Fetch Style
|--------------------------------------------------------------------------
|
| By default, database results will be returned as instances of the PHP
| stdClass object; however, you may desire to retrieve records in an
| array format for simplicity. Here you can tweak the fetch style.
|
*/
'fetch' => PDO::FETCH_CLASS,
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'testing' => [
'driver' => 'sqlite',
'database' => ':memory:',
],
'sqlite' => [
'driver' => 'sqlite',
'database' => base_path('tests/database.sqlite'),
'prefix' => '',
],
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => false,
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'cluster' => false,
'default' => [
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
],
],
];
================================================
FILE: config/filesystems.php
================================================
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. A "local" driver, as well as a variety of cloud
| based drivers are available for your choosing. Just store away!
|
| Supported: "local", "ftp", "s3", "rackspace"
|
*/
'default' => 'local',
/*
|--------------------------------------------------------------------------
| Default Cloud Filesystem Disk
|--------------------------------------------------------------------------
|
| Many applications store files both locally and in the cloud. For this
| reason, you may specify a default "cloud" driver here. This driver
| will be bound as the Cloud disk implementation in the container.
|
*/
'cloud' => 's3',
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Here you may configure as many filesystem "disks" as you wish, and you
| may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options.
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'ftp' => [
'driver' => 'ftp',
'host' => 'ftp.example.com',
'username' => 'your-username',
'password' => 'your-password',
// Optional FTP Settings...
// 'port' => 21,
// 'root' => '',
// 'passive' => true,
// 'ssl' => true,
// 'timeout' => 30,
],
's3' => [
'd
gitextract_5wn34sgq/
├── .bowerrc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── Envoy.blade.php
├── LICENSE
├── Vagrantfile
├── apiary.apib
├── app/
│ ├── Article.php
│ ├── Attachment.php
│ ├── AuthorTrait.php
│ ├── Comment.php
│ ├── Console/
│ │ ├── Commands/
│ │ │ ├── BackupDb.php
│ │ │ ├── ClearLog.php
│ │ │ ├── Inspire.php
│ │ │ ├── PruneRelease.php
│ │ │ └── UpdateLessonsTable.php
│ │ └── Kernel.php
│ ├── Events/
│ │ ├── ArticleConsumed.php
│ │ ├── Event.php
│ │ └── ModelChanged.php
│ ├── Exceptions/
│ │ └── Handler.php
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Api/
│ │ │ │ ├── PasswordsController.php
│ │ │ │ ├── SessionsController.php
│ │ │ │ ├── UsersController.php
│ │ │ │ ├── V1/
│ │ │ │ │ ├── ArticlesController.php
│ │ │ │ │ ├── CommentsController.php
│ │ │ │ │ └── WelcomeController.php
│ │ │ │ └── WelcomeController.php
│ │ │ ├── ArticlesController.php
│ │ │ ├── AttachmentsController.php
│ │ │ ├── Cacheable.php
│ │ │ ├── CommentsController.php
│ │ │ ├── Controller.php
│ │ │ ├── LessonsController.php
│ │ │ ├── PasswordsController.php
│ │ │ ├── SessionsController.php
│ │ │ ├── SocialController.php
│ │ │ ├── UsersController.php
│ │ │ └── WelcomeController.php
│ │ ├── Kernel.php
│ │ ├── Middleware/
│ │ │ ├── Authenticate.php
│ │ │ ├── AuthorOnly.php
│ │ │ ├── EncryptCookies.php
│ │ │ ├── GetUserFromToken.php
│ │ │ ├── ObfuscateId.php
│ │ │ ├── RedirectIfAuthenticated.php
│ │ │ ├── RefreshToken.php
│ │ │ ├── ThrottleApiRequests.php
│ │ │ └── VerifyCsrfToken.php
│ │ ├── Requests/
│ │ │ ├── ArticlesRequest.php
│ │ │ ├── FilterArticlesRequest.php
│ │ │ └── Request.php
│ │ └── routes.php
│ ├── Jobs/
│ │ └── Job.php
│ ├── Lesson.php
│ ├── Listeners/
│ │ ├── .gitkeep
│ │ ├── CacheHandler.php
│ │ ├── CommentsHandler.php
│ │ ├── UserEventsHandler.php
│ │ └── ViewCountHandler.php
│ ├── Model.php
│ ├── Policies/
│ │ └── .gitkeep
│ ├── Providers/
│ │ ├── AppServiceProvider.php
│ │ ├── AuthServiceProvider.php
│ │ ├── EventServiceProvider.php
│ │ └── RouteServiceProvider.php
│ ├── Reporters/
│ │ ├── ErrorReport.php
│ │ └── MonologSlackReport.php
│ ├── Repositories/
│ │ ├── LessonRepository.php
│ │ ├── MarkdownRepository.php
│ │ └── RepositoryInterface.php
│ ├── Services/
│ │ └── Markdown.php
│ ├── Tag.php
│ ├── Transformers/
│ │ ├── ArticleTransformer.php
│ │ ├── AttachmentTransformer.php
│ │ ├── CommentTransformer.php
│ │ ├── TagTransformer.php
│ │ └── UserTransformer.php
│ ├── User.php
│ ├── Vote.php
│ └── helpers.php
├── artisan
├── bootstrap/
│ ├── app.php
│ ├── autoload.php
│ └── cache/
│ └── .gitignore
├── bower.json
├── composer.json
├── config/
│ ├── api.php
│ ├── app.php
│ ├── auth.php
│ ├── broadcasting.php
│ ├── cache.php
│ ├── compile.php
│ ├── cors.php
│ ├── database.php
│ ├── filesystems.php
│ ├── icons.php
│ ├── jwt.php
│ ├── mail.php
│ ├── project.php
│ ├── queue.php
│ ├── roles.php
│ ├── services.php
│ ├── session.php
│ ├── slack.php
│ └── view.php
├── database/
│ ├── .gitignore
│ ├── factories/
│ │ └── ModelFactory.php
│ ├── migrations/
│ │ ├── .gitkeep
│ │ ├── 2014_10_12_000000_create_users_table.php
│ │ ├── 2014_10_12_100000_create_password_resets_table.php
│ │ ├── 2015_01_15_105324_create_roles_table.php
│ │ ├── 2015_01_15_114412_create_role_user_table.php
│ │ ├── 2015_01_26_115212_create_permissions_table.php
│ │ ├── 2015_01_26_115523_create_permission_role_table.php
│ │ ├── 2015_02_09_132439_create_permission_user_table.php
│ │ ├── 2015_11_20_062500_create_comments_table.php
│ │ ├── 2015_11_20_062513_create_articles_table.php
│ │ ├── 2015_11_20_062601_create_tags_table.php
│ │ ├── 2015_11_20_062613_create_attachments_table.php
│ │ ├── 2015_11_20_062846_create_article_tag_table.php
│ │ ├── 2015_12_09_165930_create_lessons_table.php
│ │ └── 2015_12_10_151357_create_votes_table.php
│ └── seeds/
│ ├── .gitkeep
│ └── DatabaseSeeder.php
├── gulpfile.js
├── lessons/
│ ├── 01-welcome.md
│ ├── 02-hello-laravel.md
│ ├── 02-install-homestead-osx.md
│ ├── 02-install-homestead-windows.md
│ ├── 02-install-on-windows.md
│ ├── 03-configuration.md
│ ├── 04-routing-basics.md
│ ├── 05-pass-data-to-view.md
│ ├── 06-blade-101.md
│ ├── 07-blade-201.md
│ ├── 08-raw-queries.md
│ ├── 09-query-builder.md
│ ├── 10-eloquent.md
│ ├── 11-migration.md
│ ├── 12-controller.md
│ ├── 13-restful-resource-controller.md
│ ├── 14-named-routes.md
│ ├── 15-nested-resources.md
│ ├── 16-authentication.md
│ ├── 17-authentication-201.md
│ ├── 18-eloquent-relationships.md
│ ├── 19-seeder.md
│ ├── 20-1-pagination.md
│ ├── 20-eager-loading.md
│ ├── 21-mail.md
│ ├── 22-events.md
│ ├── 23-validation.md
│ ├── 24-exception-handling.md
│ ├── 25-composer.md
│ ├── 26-document-model.md
│ ├── 27-document-controller.md
│ ├── 28-cache.md
│ ├── 29-elixir.md
│ ├── 30-final-touch.md
│ ├── 31-forum-features.md
│ ├── 32-login.md
│ ├── 32n33-auth-refactoring.md
│ ├── 33-social-login.md
│ ├── 34-role.md
│ ├── 35-locale.md
│ ├── 36-models.md
│ ├── 37-articles.md
│ ├── 38-tags.md
│ ├── 39-attachments.md
│ ├── 40-comments.md
│ ├── 41-ui-makeup.md
│ ├── 42-be-makeup.md
│ ├── 43-change-note.md
│ ├── 44-api-basic.md
│ ├── 45-api-big-picture.md
│ ├── 46-jwt.md
│ ├── 47-dry-refactoring.md
│ ├── 48-all-is-bad.md
│ ├── 49-rate-limit.md
│ ├── 50-id-obfuscation.md
│ ├── 51-cors.md
│ ├── 52-caching.md
│ ├── 53-partial-response.md
│ ├── 54-api-docs.md
│ ├── 999-code-release.md
│ ├── INDEX.md
│ └── images/
│ ├── 02-hello-laravel-img-01.pptx
│ ├── 02-hello-laravel-img-03.pptx
│ ├── 32n33-auth-refactoring-img-02.pptx
│ └── 46-jwt-img-01.pptx
├── package.json
├── phpunit.xml
├── public/
│ ├── .htaccess
│ ├── attachments/
│ │ └── .gitignore
│ ├── build/
│ │ ├── css/
│ │ │ └── app-4cd4d601dd.css
│ │ ├── fonts/
│ │ │ └── FontAwesome.otf
│ │ ├── js/
│ │ │ ├── app-038b8ad709.js
│ │ │ └── app-6c3ef62a70.js
│ │ └── rev-manifest.json
│ ├── index.php
│ └── robots.txt
├── readme.md
├── resources/
│ ├── assets/
│ │ ├── js/
│ │ │ └── app.js
│ │ └── sass/
│ │ ├── _auth.scss
│ │ ├── _commons.scss
│ │ ├── _forum.scss
│ │ ├── _landing.scss
│ │ ├── _lessons.scss
│ │ ├── _mediaquery.scss
│ │ └── app.scss
│ ├── lang/
│ │ ├── en/
│ │ │ ├── auth.php
│ │ │ ├── common.php
│ │ │ ├── errors.php
│ │ │ ├── forum.php
│ │ │ ├── lessons.php
│ │ │ ├── pagination.php
│ │ │ ├── passwords.php
│ │ │ └── validation.php
│ │ └── ko/
│ │ ├── auth.php
│ │ ├── common.php
│ │ ├── errors.php
│ │ ├── forum.php
│ │ ├── lessons.php
│ │ ├── pagination.php
│ │ ├── passwords.php
│ │ └── validation.php
│ └── views/
│ ├── articles/
│ │ ├── create.blade.php
│ │ ├── edit.blade.php
│ │ ├── index.blade.php
│ │ ├── partial/
│ │ │ ├── article.blade.php
│ │ │ ├── form.blade.php
│ │ │ └── search.blade.php
│ │ └── show.blade.php
│ ├── attachments/
│ │ └── partial/
│ │ └── list.blade.php
│ ├── comments/
│ │ ├── index.blade.php
│ │ └── partial/
│ │ ├── best.blade.php
│ │ ├── comment.blade.php
│ │ ├── control.blade.php
│ │ ├── create.blade.php
│ │ ├── edit.blade.php
│ │ └── login.blade.php
│ ├── emails/
│ │ ├── new-comment.blade.php
│ │ └── password.blade.php
│ ├── errors/
│ │ ├── 503.blade.php
│ │ └── notice.blade.php
│ ├── home.blade.php
│ ├── layouts/
│ │ ├── master.blade.php
│ │ └── partial/
│ │ ├── flash_message.blade.php
│ │ ├── flash_modal.blade.php
│ │ ├── footer.blade.php
│ │ ├── markdown.blade.php
│ │ ├── navigation.blade.php
│ │ └── tracker.blade.php
│ ├── lessons/
│ │ ├── partial/
│ │ │ └── pager.blade.php
│ │ └── show.blade.php
│ ├── passwords/
│ │ ├── remind.blade.php
│ │ └── reset.blade.php
│ ├── sessions/
│ │ └── create.blade.php
│ ├── tags/
│ │ └── partial/
│ │ ├── index.blade.php
│ │ └── list.blade.php
│ ├── users/
│ │ ├── create.blade.php
│ │ └── partial/
│ │ └── avatar.blade.php
│ └── vendor/
│ ├── .gitkeep
│ └── flash/
│ ├── message.blade.php
│ └── modal.blade.php
├── server.php
├── storage/
│ ├── app/
│ │ └── .gitignore
│ ├── backup/
│ │ └── .gitignore
│ ├── framework/
│ │ ├── .gitignore
│ │ ├── cache/
│ │ │ └── .gitignore
│ │ ├── sessions/
│ │ │ └── .gitignore
│ │ └── views/
│ │ └── .gitignore
│ └── logs/
│ └── .gitignore
└── tests/
├── TestCase.php
└── integration/
└── Http/
└── Controllers/
├── Api/
│ ├── ApiTest.php
│ ├── PasswordsController.php
│ ├── SessionsController.php
│ ├── UsersController.php
│ └── V1/
│ └── ArticlesController.php
├── AuthTest.php
├── SessionsController.php
├── UsersController.php
└── WelcomeController.php
Condensed preview — 287 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,284K chars).
[
{
"path": ".bowerrc",
"chars": 58,
"preview": "{\"directory\":\"resources/assets/vendor\",\"analytics\":false}\n"
},
{
"path": ".editorconfig",
"chars": 280,
"preview": "# editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-"
},
{
"path": ".gitattributes",
"chars": 61,
"preview": "* text=auto\n*.css linguist-vendored\n*.less linguist-vendored\n"
},
{
"path": ".gitignore",
"chars": 198,
"preview": ".env\n.idea\n.DS_Store\n.phpstorm*\n_ide_helper*\ndredd.*\ngit_*\nHomestead.*\nnode_modules\nnpm-debug.log\n/public/css\n/public/js"
},
{
"path": ".travis.yml",
"chars": 548,
"preview": "language: php\n\nphp:\n - 5.6\n - 7.0\n\nsudo: false\n\ninstall:\n - composer self-update\n - travis_retry composer install --"
},
{
"path": "CONTRIBUTING.md",
"chars": 791,
"preview": "# Contribution Guide\n\n이 강좌에 여러가지 방법으로 기여할 수 있습니다. \n\n- 강좌 오류 수정\n- 코드 오류 수정\n- ...\n\n기여자 분들은 기여의 형태에 따라 [Contributors "
},
{
"path": "Envoy.blade.php",
"chars": 4982,
"preview": "#--------------------------------------------------------------------------\n# List of tasks, that you can run...\n# e.g. "
},
{
"path": "LICENSE",
"chars": 1089,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Appkr <juwonkim@me.com>\n\nPermission is hereby granted, free of charge, to any "
},
{
"path": "Vagrantfile",
"chars": 900,
"preview": "require 'json'\nrequire 'yaml'\n\nVAGRANTFILE_API_VERSION ||= \"2\"\nconfDir = $confDir ||= File.expand_path(\"vendor/laravel/h"
},
{
"path": "apiary.apib",
"chars": 25131,
"preview": "FORMAT: 1A\nHOST: http://api.appkr.kr\n\n# Welcome to the myProject Api\n\nmyProject Api 에 오신 것을 환영합니다.\n\nmyProject 에서는 포럼 <su"
},
{
"path": "app/Article.php",
"chars": 2050,
"preview": "<?php\n\nnamespace App;\n\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\nclass Article extends Model\n{\n use SoftDeletes;"
},
{
"path": "app/Attachment.php",
"chars": 280,
"preview": "<?php\n\nnamespace App;\n\nclass Attachment extends Model\n{\n protected $fillable = [\n 'name'\n ];\n\n protected"
},
{
"path": "app/AuthorTrait.php",
"chars": 261,
"preview": "<?php\n\nnamespace App;\n\ntrait AuthorTrait\n{\n /**\n * Determine if the current instance was authored by the current "
},
{
"path": "app/Comment.php",
"chars": 1407,
"preview": "<?php\n\nnamespace App;\n\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\n\nclass Comment extends Model\n{\n use SoftDeletes;"
},
{
"path": "app/Console/Commands/BackupDb.php",
"chars": 1414,
"preview": "<?php\n\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\n\nclass BackupDb extends Command\n{\n /**\n *"
},
{
"path": "app/Console/Commands/ClearLog.php",
"chars": 786,
"preview": "<?php\n\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\n\nclass ClearLog extends Command\n{\n /**\n *"
},
{
"path": "app/Console/Commands/Inspire.php",
"chars": 602,
"preview": "<?php\n\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Foundation\\Inspiring;\n\nclass Insp"
},
{
"path": "app/Console/Commands/PruneRelease.php",
"chars": 1342,
"preview": "<?php\n\nnamespace App\\Console\\Commands;\n\nuse File;\nuse Illuminate\\Console\\Command;\n\nclass PruneRelease extends Command\n{\n"
},
{
"path": "app/Console/Commands/UpdateLessonsTable.php",
"chars": 1043,
"preview": "<?php\n\nnamespace App\\Console\\Commands;\n\nuse App\\DocumentRepository;\nuse Illuminate\\Console\\Command;\n\nclass UpdateLessons"
},
{
"path": "app/Console/Kernel.php",
"chars": 1063,
"preview": "<?php\n\nnamespace App\\Console;\n\nuse Illuminate\\Console\\Scheduling\\Schedule;\nuse Illuminate\\Foundation\\Console\\Kernel as C"
},
{
"path": "app/Events/ArticleConsumed.php",
"chars": 397,
"preview": "<?php\n\nnamespace App\\Events;\n\nuse Illuminate\\Queue\\SerializesModels;\n\nclass ArticleConsumed extends Event\n{\n use Seri"
},
{
"path": "app/Events/Event.php",
"chars": 62,
"preview": "<?php\n\nnamespace App\\Events;\n\nabstract class Event\n{\n //\n}\n"
},
{
"path": "app/Events/ModelChanged.php",
"chars": 385,
"preview": "<?php\n\nnamespace App\\Events;\n\nuse Illuminate\\Queue\\SerializesModels;\n\nclass ModelChanged extends Event\n{\n use Seriali"
},
{
"path": "app/Exceptions/Handler.php",
"chars": 3675,
"preview": "<?php\n\nnamespace App\\Exceptions;\n\nuse Exception;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Datab"
},
{
"path": "app/Http/Controllers/Api/PasswordsController.php",
"chars": 910,
"preview": "<?php\n\nnamespace App\\Http\\Controllers\\Api;\n\nuse App\\Http\\Controllers\\PasswordsController as ParentController;\n\nclass Pas"
},
{
"path": "app/Http/Controllers/Api/SessionsController.php",
"chars": 1491,
"preview": "<?php\n\nnamespace App\\Http\\Controllers\\Api;\n\nuse App\\Http\\Controllers\\SessionsController as ParentController;\nuse Illumin"
},
{
"path": "app/Http/Controllers/Api/UsersController.php",
"chars": 1377,
"preview": "<?php\n\nnamespace App\\Http\\Controllers\\Api;\n\nuse App\\Http\\Controllers\\UsersController as ParentController;\nuse App\\User;\n"
},
{
"path": "app/Http/Controllers/Api/V1/ArticlesController.php",
"chars": 3035,
"preview": "<?php\n\nnamespace App\\Http\\Controllers\\Api\\V1;\n\nuse App\\Article;\nuse App\\Transformers\\ArticleTransformer;\nuse App\\Http\\Co"
},
{
"path": "app/Http/Controllers/Api/V1/CommentsController.php",
"chars": 841,
"preview": "<?php\n\nnamespace App\\Http\\Controllers\\Api\\V1;\n\nuse App\\Http\\Controllers\\Controller;\nuse App\\Transformers\\CommentTransfor"
},
{
"path": "app/Http/Controllers/Api/V1/WelcomeController.php",
"chars": 1079,
"preview": "<?php\n\nnamespace App\\Http\\Controllers\\Api\\V1;\n\nuse App\\Http\\Controllers\\Controller;\n\nclass WelcomeController extends Con"
},
{
"path": "app/Http/Controllers/Api/WelcomeController.php",
"chars": 1199,
"preview": "<?php\n\nnamespace App\\Http\\Controllers\\Api;\n\nuse App\\Http\\Controllers\\Controller;\n\nclass WelcomeController extends Contro"
},
{
"path": "app/Http/Controllers/ArticlesController.php",
"chars": 8469,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Article;\nuse App\\Events\\ArticleConsumed;\nuse App\\Events\\ModelChanged;\nus"
},
{
"path": "app/Http/Controllers/AttachmentsController.php",
"chars": 1951,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Events\\ModelChanged;\nuse Illuminate\\Http\\Request;\nuse App\\Http\\Requests;"
},
{
"path": "app/Http/Controllers/Cacheable.php",
"chars": 232,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\ninterface Cacheable\n{\n /**\n * Specify the tags for caching.\n * @see h"
},
{
"path": "app/Http/Controllers/CommentsController.php",
"chars": 4854,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Comment;\nuse App\\Vote;\nuse App\\Events\\ModelChanged;\nuse Illuminate\\Http\\"
},
{
"path": "app/Http/Controllers/Controller.php",
"chars": 3599,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse Illuminate\\Foundation\\Bus\\DispatchesJobs;\nuse Illuminate\\Routing\\Controller "
},
{
"path": "app/Http/Controllers/LessonsController.php",
"chars": 2260,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Document;\nuse App\\Repositories\\LessonRepository;\nuse Request;\n\nclass Les"
},
{
"path": "app/Http/Controllers/PasswordsController.php",
"chars": 3854,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\User;\nuse Illuminate\\Http\\Request;\nuse Password;\n\nclass PasswordsControl"
},
{
"path": "app/Http/Controllers/SessionsController.php",
"chars": 2795,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse Auth;\nuse Illuminate\\Contracts\\Validation\\Validator;\nuse Illuminate\\Http\\Req"
},
{
"path": "app/Http/Controllers/SocialController.php",
"chars": 2036,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\User;\nuse Illuminate\\Http\\Request;\nuse Laravel\\Socialite\\Contracts\\Facto"
},
{
"path": "app/Http/Controllers/UsersController.php",
"chars": 4055,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\User;\nuse Illuminate\\Contracts\\Validation\\Validator;\nuse Illuminate\\Http"
},
{
"path": "app/Http/Controllers/WelcomeController.php",
"chars": 1063,
"preview": "<?php\n\nnamespace App\\Http\\Controllers;\n\nclass WelcomeController extends Controller\n{\n /**\n * Constructor.\n */"
},
{
"path": "app/Http/Kernel.php",
"chars": 2004,
"preview": "<?php\n\nnamespace App\\Http;\n\nuse Illuminate\\Foundation\\Http\\Kernel as HttpKernel;\n\nclass Kernel extends HttpKernel\n{\n "
},
{
"path": "app/Http/Middleware/Authenticate.php",
"chars": 974,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Contracts\\Auth\\Guard;\n\nclass Authenticate\n{\n /**\n "
},
{
"path": "app/Http/Middleware/AuthorOnly.php",
"chars": 937,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\n\nclass AuthorOnly\n{\n /**\n * Hand"
},
{
"path": "app/Http/Middleware/EncryptCookies.php",
"chars": 300,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Illuminate\\Cookie\\Middleware\\EncryptCookies as BaseEncrypter;\n\nclass EncryptC"
},
{
"path": "app/Http/Middleware/GetUserFromToken.php",
"chars": 812,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Tymon\\JWTAuth\\Exceptions\\JWTException;\nuse Tymon\\JWTAuth\\Middleware\\BaseMiddl"
},
{
"path": "app/Http/Middleware/ObfuscateId.php",
"chars": 697,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\n\nclass ObfuscateId\n{\n /**\n * Han"
},
{
"path": "app/Http/Middleware/RedirectIfAuthenticated.php",
"chars": 756,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Contracts\\Auth\\Guard;\n\nclass RedirectIfAuthenticated\n"
},
{
"path": "app/Http/Middleware/RefreshToken.php",
"chars": 671,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Tymon\\JWTAuth\\Middleware\\BaseMiddleware;\n\nclass RefreshToken extends BaseMidd"
},
{
"path": "app/Http/Middleware/ThrottleApiRequests.php",
"chars": 1188,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Routing\\Middleware\\ThrottleRequests;\n\nclass ThrottleA"
},
{
"path": "app/Http/Middleware/VerifyCsrfToken.php",
"chars": 537,
"preview": "<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken as BaseVer"
},
{
"path": "app/Http/Requests/ArticlesRequest.php",
"chars": 776,
"preview": "<?php\n\nnamespace App\\Http\\Requests;\n\nclass ArticlesRequest extends Request\n{\n /**\n * Determine if the user is aut"
},
{
"path": "app/Http/Requests/FilterArticlesRequest.php",
"chars": 1126,
"preview": "<?php\n\nnamespace App\\Http\\Requests;\n\nclass FilterArticlesRequest extends Request\n{\n /**\n * Determine if the user "
},
{
"path": "app/Http/Requests/Request.php",
"chars": 1596,
"preview": "<?php\n\nnamespace App\\Http\\Requests;\n\nuse Illuminate\\Foundation\\Http\\FormRequest;\n\nabstract class Request extends FormReq"
},
{
"path": "app/Http/routes.php",
"chars": 5255,
"preview": "<?php\n\nRoute::group(['domain' => config('project.api_domain'), 'as' => 'api.', 'namespace' => 'Api', 'middleware' => 'co"
},
{
"path": "app/Jobs/Job.php",
"chars": 535,
"preview": "<?php\n\nnamespace App\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\n\nabstract class Job\n{\n /*\n |----------------------------"
},
{
"path": "app/Lesson.php",
"chars": 823,
"preview": "<?php\n\nnamespace App;\n\nclass Lesson extends Model\n{\n use AuthorTrait;\n\n // Directory name that holds markdown file"
},
{
"path": "app/Listeners/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "app/Listeners/CacheHandler.php",
"chars": 465,
"preview": "<?php\n\nnamespace App\\Listeners;\n\nuse App\\Events\\ModelChanged;\n\nclass CacheHandler\n{\n /**\n * Handle the event.\n "
},
{
"path": "app/Listeners/CommentsHandler.php",
"chars": 1247,
"preview": "<?php\n\nnamespace App\\Listeners;\n\nuse App\\Comment;\n\nclass CommentsHandler\n{\n protected $to = [];\n\n /**\n * Handl"
},
{
"path": "app/Listeners/UserEventsHandler.php",
"chars": 638,
"preview": "<?php\n\nnamespace App\\Listeners;\n\nuse App\\User;\nuse Illuminate\\Contracts\\Events\\Dispatcher;\n\nclass UserEventsHandler\n{\n "
},
{
"path": "app/Listeners/ViewCountHandler.php",
"chars": 367,
"preview": "<?php\n\nnamespace App\\Listeners;\n\nuse App\\Events\\ArticleConsumed;\n\nclass ViewCountHandler\n{\n /**\n * Handle the eve"
},
{
"path": "app/Model.php",
"chars": 118,
"preview": "<?php\n\nnamespace App;\n\nuse Illuminate\\Database\\Eloquent\\Model as Eloquent;\n\nabstract class Model extends Eloquent\n{\n\n}"
},
{
"path": "app/Policies/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "app/Providers/AppServiceProvider.php",
"chars": 897,
"preview": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\ServiceProvider;\n\nclass AppServiceProvider extends ServiceProvid"
},
{
"path": "app/Providers/AuthServiceProvider.php",
"chars": 693,
"preview": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Contracts\\Auth\\Access\\Gate as GateContract;\nuse Illuminate\\Foundation\\Su"
},
{
"path": "app/Providers/EventServiceProvider.php",
"chars": 989,
"preview": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Contracts\\Events\\Dispatcher as DispatcherContract;\nuse Illuminate\\Founda"
},
{
"path": "app/Providers/RouteServiceProvider.php",
"chars": 1240,
"preview": "<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Support\\Providers\\RouteService"
},
{
"path": "app/Reporters/ErrorReport.php",
"chars": 3273,
"preview": "<?php\n\nnamespace App\\Reporters;\n\nuse Carbon\\Carbon;\nuse Maknz\\Slack\\Attachment;\nuse Maknz\\Slack\\AttachmentField;\nuse Mak"
},
{
"path": "app/Reporters/MonologSlackReport.php",
"chars": 1970,
"preview": "<?php\n\nnamespace App\\Reporters;\n\nclass MonologSlackReport\n{\n /**\n * @var \\Monolog\\Logger\n */\n protected $l"
},
{
"path": "app/Repositories/LessonRepository.php",
"chars": 416,
"preview": "<?php\n\nnamespace App\\Repositories;\n\nclass LessonRepository extends MarkdownRepository\n{\n /**\n * Get the index of "
},
{
"path": "app/Repositories/MarkdownRepository.php",
"chars": 5091,
"preview": "<?php\n\nnamespace App\\Repositories;\n\nuse Cache;\nuse Exception;\nuse File;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Imag"
},
{
"path": "app/Repositories/RepositoryInterface.php",
"chars": 464,
"preview": "<?php\n\nnamespace App\\Repositories;\n\ninterface RepositoryInterface\n{\n /**\n * Get the model instance.\n *\n *"
},
{
"path": "app/Services/Markdown.php",
"chars": 1413,
"preview": "<?php\n\nnamespace App\\Services;\n\nuse ParsedownExtra;\n\nclass Markdown extends ParsedownExtra {\n\n // Pattern to search f"
},
{
"path": "app/Tag.php",
"chars": 257,
"preview": "<?php\n\nnamespace App;\n\nclass Tag extends Model\n{\n protected $fillable = [\n 'name',\n 'slug'\n ];\n\n "
},
{
"path": "app/Transformers/ArticleTransformer.php",
"chars": 4125,
"preview": "<?php\n\nnamespace App\\Transformers;\n\nuse App\\Article;\nuse Appkr\\Api\\TransformerAbstract;\nuse League\\Fractal\\ParamBag;\n\ncl"
},
{
"path": "app/Transformers/AttachmentTransformer.php",
"chars": 911,
"preview": "<?php\n\nnamespace App\\Transformers;\n\nuse App\\Attachment;\nuse Appkr\\Api\\TransformerAbstract;\nuse League\\Fractal\\ParamBag;\n"
},
{
"path": "app/Transformers/CommentTransformer.php",
"chars": 1951,
"preview": "<?php\n\nnamespace App\\Transformers;\n\nuse App\\Comment;\nuse Appkr\\Api\\TransformerAbstract;\n\n;\nuse League\\Fractal\\ParamBag;\n"
},
{
"path": "app/Transformers/TagTransformer.php",
"chars": 1547,
"preview": "<?php\n\nnamespace App\\Transformers;\n\nuse App\\Tag;\nuse Appkr\\Api\\TransformerAbstract;\nuse League\\Fractal\\ParamBag;\n\nclass "
},
{
"path": "app/Transformers/UserTransformer.php",
"chars": 2624,
"preview": "<?php\n\nnamespace App\\Transformers;\n\nuse App\\User;\nuse Appkr\\Api\\TransformerAbstract;\nuse League\\Fractal\\ParamBag;\n\nclass"
},
{
"path": "app/User.php",
"chars": 1687,
"preview": "<?php\n\nnamespace App;\n\nuse Illuminate\\Auth\\Authenticatable;\nuse Illuminate\\Auth\\Passwords\\CanResetPassword;\nuse Illumina"
},
{
"path": "app/Vote.php",
"chars": 467,
"preview": "<?php\n\nnamespace App;\n\nclass Vote extends Model\n{\n public $timestamps = false;\n\n protected $fillable = [\n '"
},
{
"path": "app/helpers.php",
"chars": 4612,
"preview": "<?php\n\nif (! function_exists('markdown')) {\n /**\n * Compile the given text to markdown document.\n *\n * @p"
},
{
"path": "artisan",
"chars": 1646,
"preview": "#!/usr/bin/env php\n<?php\n\n/*\n|--------------------------------------------------------------------------\n| Register The "
},
{
"path": "bootstrap/app.php",
"chars": 1602,
"preview": "<?php\n\n/*\n|--------------------------------------------------------------------------\n| Create The Application\n|--------"
},
{
"path": "bootstrap/autoload.php",
"chars": 1079,
"preview": "<?php\n\ndefine('LARAVEL_START', microtime(true));\n\n/*\n|------------------------------------------------------------------"
},
{
"path": "bootstrap/cache/.gitignore",
"chars": 14,
"preview": "*\n!.gitignore\n"
},
{
"path": "bower.json",
"chars": 345,
"preview": "{\n \"name\": \"myProject\",\n \"devDependencies\": {\n \"bootstrap-sass\": \"~3.3.5\",\n \"font-awesome\": \"~4.4.0\",\n \"dropz"
},
{
"path": "composer.json",
"chars": 1808,
"preview": "{\n \"name\": \"appkr/l5essential\",\n \"description\": \"Laravel 5 Essential\",\n \"keywords\": [\n \"laravel\",\n \"how-to\",\n "
},
{
"path": "config/api.php",
"chars": 5077,
"preview": "<?php\n\nreturn [\n /*\n |--------------------------------------------------------------------------\n | Debug\n |"
},
{
"path": "config/app.php",
"chars": 9666,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Applicatio"
},
{
"path": "config/auth.php",
"chars": 3635,
"preview": "<?php\n\n// Copied from https://github.com/laravel/laravel/blob/master/config/auth.php\n// while migrating to Laravel 5.2\n\n"
},
{
"path": "config/broadcasting.php",
"chars": 1404,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Default Br"
},
{
"path": "config/cache.php",
"chars": 2141,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Default Ca"
},
{
"path": "config/compile.php",
"chars": 983,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Additional"
},
{
"path": "config/cors.php",
"chars": 624,
"preview": "<?php\n\nreturn [\n /*\n |--------------------------------------------------------------------------\n | Laravel C"
},
{
"path": "config/database.php",
"chars": 4200,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | PDO Fetch "
},
{
"path": "config/filesystems.php",
"chars": 2546,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Default Fi"
},
{
"path": "config/icons.php",
"chars": 1268,
"preview": "<?php\n\nreturn [\n\n 'login' => 'fa fa-sign-in',\n 'github' => 'fa fa-github',\n 'book' => 'fa fa-"
},
{
"path": "config/jwt.php",
"chars": 5244,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | JWT Authen"
},
{
"path": "config/mail.php",
"chars": 4444,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Mail Drive"
},
{
"path": "config/project.php",
"chars": 3373,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Enable or "
},
{
"path": "config/queue.php",
"chars": 2712,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Default Qu"
},
{
"path": "config/roles.php",
"chars": 2057,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Package Co"
},
{
"path": "config/services.php",
"chars": 1166,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Third Part"
},
{
"path": "config/session.php",
"chars": 5297,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | Default Se"
},
{
"path": "config/slack.php",
"chars": 3241,
"preview": "<?php\n\nreturn [\n\n /*\n |-------------------------------------------------------------\n | Incoming webhook endpoint\n |"
},
{
"path": "config/view.php",
"chars": 1020,
"preview": "<?php\n\nreturn [\n\n /*\n |--------------------------------------------------------------------------\n | View Stora"
},
{
"path": "database/.gitignore",
"chars": 9,
"preview": "*.sqlite\n"
},
{
"path": "database/factories/ModelFactory.php",
"chars": 1499,
"preview": "<?php\n\n/*\n|--------------------------------------------------------------------------\n| Model Factories\n|---------------"
},
{
"path": "database/migrations/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "database/migrations/2014_10_12_000000_create_users_table.php",
"chars": 772,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateUsersTable e"
},
{
"path": "database/migrations/2014_10_12_100000_create_password_resets_table.php",
"chars": 633,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreatePasswordRese"
},
{
"path": "database/migrations/2015_01_15_105324_create_roles_table.php",
"chars": 738,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateRolesTable e"
},
{
"path": "database/migrations/2015_01_15_114412_create_role_user_table.php",
"chars": 868,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateRoleUserTabl"
},
{
"path": "database/migrations/2015_01_26_115212_create_permissions_table.php",
"chars": 755,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreatePermissionsT"
},
{
"path": "database/migrations/2015_01_26_115523_create_permission_role_table.php",
"chars": 904,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreatePermissionRo"
},
{
"path": "database/migrations/2015_02_09_132439_create_permission_user_table.php",
"chars": 904,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreatePermissionUs"
},
{
"path": "database/migrations/2015_11_20_062500_create_comments_table.php",
"chars": 1076,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateCommentsTabl"
},
{
"path": "database/migrations/2015_11_20_062513_create_articles_table.php",
"chars": 1277,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateArticlesTabl"
},
{
"path": "database/migrations/2015_11_20_062601_create_tags_table.php",
"chars": 617,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateTagsTable ex"
},
{
"path": "database/migrations/2015_11_20_062613_create_attachments_table.php",
"chars": 758,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateAttachmentsT"
},
{
"path": "database/migrations/2015_11_20_062846_create_article_tag_table.php",
"chars": 851,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateArticleTagTa"
},
{
"path": "database/migrations/2015_12_09_165930_create_lessons_table.php",
"chars": 894,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateLessonsTable"
},
{
"path": "database/migrations/2015_12_10_151357_create_votes_table.php",
"chars": 950,
"preview": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nclass CreateVotesTable e"
},
{
"path": "database/seeds/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "database/seeds/DatabaseSeeder.php",
"chars": 4386,
"preview": "<?php\n\nuse Illuminate\\Database\\Seeder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Faker\\Factory as Faker;\n\nclass Databa"
},
{
"path": "gulpfile.js",
"chars": 1027,
"preview": "var elixir = require('laravel-elixir');\n\nelixir(function (mix) {\n mix.sass('app.scss', 'resources/assets/css')\n .sty"
},
{
"path": "lessons/01-welcome.md",
"chars": 3059,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 0\n---\n\n# 1강 - 처음 만나는 라라벨\n\n라라벨은 php 언어로 짜여진 MVC 아키텍처를 지원하는 웹"
},
{
"path": "lessons/02-hello-laravel.md",
"chars": 6766,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 1\n---\n\n# 2강 - 라라벨 5 설치하기 (on Mac)\n\n## 윈도우즈 사용자라면\n\n[여기를 참고]("
},
{
"path": "lessons/02-install-homestead-osx.md",
"chars": 6049,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 57\n---\n\n# Homestead 설치 (on Mac)\n\n## 사전 요구 사항\n\n[VirtualBox]("
},
{
"path": "lessons/02-install-homestead-windows.md",
"chars": 5177,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 58\n---\n\n# Homestead 설치 (on Windows)\n\n## 사전 요구 사항\n\n[VirtualB"
},
{
"path": "lessons/02-install-on-windows.md",
"chars": 3940,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 2\n---\n\n# 2강 - 라라벨 5 설치하기 (on Windows)\n\nWindows 사용자라면 Mac 으로"
},
{
"path": "lessons/03-configuration.md",
"chars": 1330,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 3\n---\n\n# 3강 - 글로벌 설정 살펴보기\n\n## Code Editor 와 DB Client\n\n- [p"
},
{
"path": "lessons/04-routing-basics.md",
"chars": 2253,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 4\n---\n\n# 4강 - Routing 기본기\n\n## 웰컴 뷰를 가지고 놀자\n\nresources/views"
},
{
"path": "lessons/05-pass-data-to-view.md",
"chars": 1570,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 5\n---\n\n# 5강 - 뷰에 데이터 바인딩하기\n\n## 블레이드 템플릿 맛보기\n\nresources/view"
},
{
"path": "lessons/06-blade-101.md",
"chars": 1706,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 6\n---\n\n# 6강 - 블레이드 101\n\n블레이드는 라라벨의 템플릿 엔진이다. 뷰 안에 포함된 블레이드 "
},
{
"path": "lessons/07-blade-201.md",
"chars": 1667,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 7\n---\n\n# 7강 - 블레이드 201\n\n## @yield, @extends, @section - 마스터"
},
{
"path": "lessons/08-raw-queries.md",
"chars": 2309,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 8\n---\n\n# 8강 - 날 쿼리 :(\n\n## 사용할 테이블을 만들자\n\n[3강 - 글로벌 설정 살펴보기]("
},
{
"path": "lessons/09-query-builder.md",
"chars": 2176,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 9\n---\n\n# 9강 - 쿼리 빌더\n\nSQL 문을 php 코드로 쓴 거라고 보면 된다. 지금은 그냥 SQL"
},
{
"path": "lessons/10-eloquent.md",
"chars": 4390,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 10\n---\n\n# 10강 - 엘로퀀트 ORM\n\n엘로퀀트는 라라벨의 ORM (Object Relational"
},
{
"path": "lessons/11-migration.md",
"chars": 4393,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 11\n---\n\n# 11강 - DB 마이그레이션\n\n마이그레이션은 데이터베이스를 위한 버전 컨트롤이라 생각하면"
},
{
"path": "lessons/12-controller.md",
"chars": 1266,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 12\n---\n\n# 12강 - 컨트롤러\n\nMVC에서 V(뷰)와 M(모델)을 살펴보았다. 이제 마지막 콤포넌트"
},
{
"path": "lessons/13-restful-resource-controller.md",
"chars": 4761,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 13\n---\n\n# 13강 - RESTful 리소스 컨트롤러\n\nREST는 이 코스 범위를 넘어가는 내용이니,"
},
{
"path": "lessons/14-named-routes.md",
"chars": 2143,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 14\n---\n\n# 14강 - 이름 있는 Route\n\nNamed Routes는 여러모로 유용하다. 컨트롤러에"
},
{
"path": "lessons/15-nested-resources.md",
"chars": 1357,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 15\n---\n\n# 15강 - 중첩된 리소스\n\n특정 리소스에 딸린 하위 리소스를 보여줘야 하는 경우가 있다."
},
{
"path": "lessons/16-authentication.md",
"chars": 3734,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 16\n---\n\n# 16강 - 사용자 인증 기본기\n\n좀 과장해서, 어떤 프로젝트든 로그인이 거의 업무량의 절"
},
{
"path": "lessons/17-authentication-201.md",
"chars": 3283,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 17\n---\n\n# 17강 - 라라벨에 내장된 사용자 인증\n\n16강에 이어 이번 강좌에서는 라라벨에 딸려서 "
},
{
"path": "lessons/18-eloquent-relationships.md",
"chars": 4608,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 18\n---\n\n# 18강 - 모델간 관계 맺기\n\n쿼리빌더 없이 여러 개의 테이블에서 Join Query하는"
},
{
"path": "lessons/19-seeder.md",
"chars": 3661,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 19\n---\n\n# 19강 - 데이터 심기\n \n앞에서 마이그레이션으로 테이블을 만들어 보았다. 만들어진 테이"
},
{
"path": "lessons/20-1-pagination.md",
"chars": 1270,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 21\n---\n\n# 페이징\n\n모델에 데이터가 많아지면 한번에 모든 레코드를 표시할 수가 없게 된다. 이때 필"
},
{
"path": "lessons/20-eager-loading.md",
"chars": 2423,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 20\n---\n\n# 20강 - Eager 로딩\n\nEager 로딩은 N+1 쿼리 문제를 해결해 주는 방법이다."
},
{
"path": "lessons/21-mail.md",
"chars": 2307,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 22\n---\n\n# 21강 - 메일 보내기\n\n## mailgun 서비스 가입하자\n\n실습을 위해 라라벨에 기본"
},
{
"path": "lessons/22-events.md",
"chars": 3736,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 23\n---\n\n# 22강 - 이벤트\n\n라라벨 이벤트 시스템은 Observer 또는 PubSub 패턴을 구현"
},
{
"path": "lessons/23-validation.md",
"chars": 4778,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 24\n---\n\n# 23강 - 입력 값 유효성 검사\n\n항상 듣는 말이다. **\"사용자가 입력한 값은 절대 신"
},
{
"path": "lessons/24-exception-handling.md",
"chars": 3126,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 25\n---\n\n# 24강 - 예외 처리\n\n## 예외 던지기\n\n라라벨에서 예외(Exception)는 컨트롤러"
},
{
"path": "lessons/25-composer.md",
"chars": 4616,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 26\n---\n\n# 25강 - 컴포저\n\n2강에서 라라벨 5 처음 설치할 때 Composer를 설치했을 것이다"
},
{
"path": "lessons/26-document-model.md",
"chars": 3781,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 27\n---\n\n# 실전 프로젝트 1 - Markdown Viewer \n\n라라벨 공식 문서와 유사하게 왼쪽 "
},
{
"path": "lessons/27-document-controller.md",
"chars": 2645,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 28\n---\n\n# 실전 프로젝트 1 - Markdown Viewer \n\n## 27강 - Document 컨"
},
{
"path": "lessons/28-cache.md",
"chars": 3774,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 29\n---\n\n# 실전 프로젝트 1 - Markdown Viewer \n\n## 28강 - Cache\n\n우리가"
},
{
"path": "lessons/29-elixir.md",
"chars": 7471,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 30\n---\n\n# 실전 프로젝트 1 - Markdown Viewer \n\n## 29강 - Elixir, 만병"
},
{
"path": "lessons/30-final-touch.md",
"chars": 6485,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 31\n---\n\n# 실전 프로젝트 1 - Markdown Viewer \n\n## 30강 - Debug & Fi"
},
{
"path": "lessons/31-forum-features.md",
"chars": 1500,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 32\n---\n\n# 실전 프로젝트 2 - Forum\n\n댓글이 가능한 간단한 포럼을 구현해 본다. 이를 통해 "
},
{
"path": "lessons/32-login.md",
"chars": 15536,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 33\n---\n\n# 실전 프로젝트 2 - Forum\n\n실습을 진행하기 전에 기존에 만들었던 파일 중, 쓰지 "
},
{
"path": "lessons/32n33-auth-refactoring.md",
"chars": 10403,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 41\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 32/33 보충 - 인증 리팩토링\n\n잠깐 쉬어가자"
},
{
"path": "lessons/33-social-login.md",
"chars": 4910,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 34\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 33강 - 소셜 로그인\n\n소셜 로그인을 지원하지 "
},
{
"path": "lessons/34-role.md",
"chars": 4050,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 35\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 34강 - 사용자 역할\n\n역할 기능을 이용해 RB"
},
{
"path": "lessons/35-locale.md",
"chars": 7634,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 36\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 35강 - 다국어 지원\n\n필자도 좀 오버했다는 생"
},
{
"path": "lessons/36-models.md",
"chars": 15916,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 37\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 36강 - 마이그레이션과 모델\n\n이번 강좌에서는 "
},
{
"path": "lessons/37-articles.md",
"chars": 18536,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 38\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 37강 - Article 기능 구현\n\n이번 강좌를"
},
{
"path": "lessons/38-tags.md",
"chars": 7084,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 39\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 38강 - Tag 기능 구현\n\n이번 강좌에서는 3"
},
{
"path": "lessons/39-attachments.md",
"chars": 13853,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 40\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 39강 - Attachment 기능 구현\n\nQ. "
},
{
"path": "lessons/40-comments.md",
"chars": 16717,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 42\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 40강 - Comment 기능 구현\n\n이번 강에서"
},
{
"path": "lessons/41-ui-makeup.md",
"chars": 10196,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 43\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 41강 - UI 개선\n\n이번 강에서 전반적인 UI"
},
{
"path": "lessons/42-be-makeup.md",
"chars": 18562,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 44\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 42강 - 서버 사이드 개선\n\n아직 추가해야 할 "
},
{
"path": "lessons/43-change-note.md",
"chars": 28840,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 45\n---\n\n# 실전 프로젝트 2 - Forum\n\n## 43강 - 변경 사항 알림\n\n2~3주 정도 다른 "
},
{
"path": "lessons/44-api-basic.md",
"chars": 8669,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 46\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n**\"실전 프로젝트 2 - Forum\"** "
},
{
"path": "lessons/45-api-big-picture.md",
"chars": 11813,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 47\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 45강 - 기본 구조 잡기\n\n앞 강좌에"
},
{
"path": "lessons/46-jwt.md",
"chars": 12965,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 48\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 46강 - JWT 를 이용한 인증\n\n#"
},
{
"path": "lessons/47-dry-refactoring.md",
"chars": 5725,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 49\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 47강 - 중복 제거 리팩토링\n\n앞 강"
},
{
"path": "lessons/48-all-is-bad.md",
"chars": 19795,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 50\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 48강 - `all()` is bad\n"
},
{
"path": "lessons/49-rate-limit.md",
"chars": 3785,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 51\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 49강 - API Rate Limit\n"
},
{
"path": "lessons/50-id-obfuscation.md",
"chars": 7291,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 52\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 50강 - 리소스 id 난독화\n\n###"
},
{
"path": "lessons/51-cors.md",
"chars": 6447,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 53\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 51강 - CORS\n\n이번 강좌에서는 "
},
{
"path": "lessons/52-caching.md",
"chars": 10283,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 54\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 52강 - Caching\n\n강좌를 시작"
},
{
"path": "lessons/53-partial-response.md",
"chars": 14431,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 55\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 53강 - Partial Respons"
},
{
"path": "lessons/54-api-docs.md",
"chars": 9933,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 56\n---\n\n# 실전 프로젝트 3 - RESTful API\n\n## 54강 - API Documents\n\n"
},
{
"path": "lessons/999-code-release.md",
"chars": 13808,
"preview": "---\nextends: _layouts.master\nsection: content\ncurrent_index: 59\n---\n\n# 코드 배포 \n\n웹 프로그래머의 삶이 여유롭지는 않다. 왜냐하면, 사용자와 접하게 되는 U"
},
{
"path": "lessons/INDEX.md",
"chars": 3413,
"preview": "- **입문코스-기본기**\n - [1강 - 처음 만나는 라라벨](/lessons/01-welcome.md)\n - [2강 - 라라벨 5 설치하기](/lessons/02-hello-laravel.m"
},
{
"path": "package.json",
"chars": 127,
"preview": "{\n \"private\": true,\n \"devDependencies\": {\n \"gulp\": \"^3.8.8\"\n },\n \"dependencies\": {\n \"laravel-elixir\": \"^3.0.0\""
},
{
"path": "phpunit.xml",
"chars": 1112,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n backupStaticAttributes=\"false\"\n b"
},
{
"path": "public/.htaccess",
"chars": 724,
"preview": "<IfModule mod_rewrite.c>\n <IfModule mod_negotiation.c>\n Options -MultiViews\n </IfModule>\n\n RewriteEngine"
},
{
"path": "public/attachments/.gitignore",
"chars": 14,
"preview": "*\n!.gitignore\n"
},
{
"path": "public/build/css/app-4cd4d601dd.css",
"chars": 202260,
"preview": "/*!\n * Bootstrap v3.3.5 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://gi"
},
{
"path": "public/build/js/app-038b8ad709.js",
"chars": 1230655,
"preview": "/*!\n * jQuery JavaScript Library v2.1.4\n * http://jquery.com/\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n *\n * Cop"
},
{
"path": "public/build/js/app-6c3ef62a70.js",
"chars": 727796,
"preview": "function flash(e,t,n){var i=$(\"div.js-flash-message\");i&&i.remove(),$(\"<div></div>\",{\"class\":\"alert alert-\"+e+\" alert-di"
},
{
"path": "public/build/rev-manifest.json",
"chars": 84,
"preview": "{\n \"css/app.css\": \"css/app-4cd4d601dd.css\",\n \"js/app.js\": \"js/app-6c3ef62a70.js\"\n}"
},
{
"path": "public/index.php",
"chars": 1786,
"preview": "<?php\n\n/**\n * Laravel - A PHP Framework For Web Artisans\n *\n * @package Laravel\n * @author Taylor Otwell <taylorotwel"
},
{
"path": "public/robots.txt",
"chars": 24,
"preview": "User-agent: *\nDisallow:\n"
}
]
// ... and 87 more files (download for full content)
About this extraction
This page contains the full source code of the appkr/l5essential GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 287 files (2.8 MB), approximately 750.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.