Selaa lähdekoodia

初回コミット

master
sosuke.iwabuchi 2 vuotta sitten
commit
79f9209ab0
100 muutettua tiedostoa jossa 4747 lisäystä ja 0 poistoa
  1. +18
    -0
      .editorconfig
  2. +63
    -0
      .env.example
  3. +11
    -0
      .gitattributes
  4. +20
    -0
      .gitignore
  5. +66
    -0
      README.md
  6. +10
    -0
      app/Codes/EnvironmentName.php
  7. +11
    -0
      app/Codes/HTTPResultCode.php
  8. +130
    -0
      app/Codes/PrefCode.php
  9. +9
    -0
      app/Codes/QueueName.php
  10. +12
    -0
      app/Codes/TaxRate.php
  11. +12
    -0
      app/Codes/UserRole.php
  12. +105
    -0
      app/Console/Commands/BaseCommand.php
  13. +60
    -0
      app/Console/Commands/HeartBeat.php
  14. +210
    -0
      app/Console/Commands/RouteListCsv.php
  15. +27
    -0
      app/Console/Kernel.php
  16. +10
    -0
      app/Console/Schedules/BaseSchedule.php
  17. +22
    -0
      app/Console/Schedules/HeartBeat.php
  18. +211
    -0
      app/Email/BaseEmailer.php
  19. +15
    -0
      app/Email/Guests/Guest.php
  20. +15
    -0
      app/Email/Members/Members.php
  21. +42
    -0
      app/Email/Sender.php
  22. +21
    -0
      app/Email/Test.php
  23. +40
    -0
      app/Email/TextEmail.php
  24. +42
    -0
      app/Events/Email/ConfirmEvent.php
  25. +8
    -0
      app/Events/Model/CreatedEvent.php
  26. +8
    -0
      app/Events/Model/DeletedEvent.php
  27. +8
    -0
      app/Events/Model/DeletingEvent.php
  28. +36
    -0
      app/Events/Model/ModelChangeEvent.php
  29. +8
    -0
      app/Events/Model/UpdatingEvent.php
  30. +9
    -0
      app/Exceptions/AppCommonException.php
  31. +13
    -0
      app/Exceptions/ConfigException.php
  32. +12
    -0
      app/Exceptions/ExclusiveException.php
  33. +9
    -0
      app/Exceptions/GeneralErrorMessageException.php
  34. +48
    -0
      app/Exceptions/Handler.php
  35. +21
    -0
      app/Exceptions/TempFileNotExistsException.php
  36. +14
    -0
      app/Features/InstanceAble.php
  37. +12
    -0
      app/Http/Controllers/Controller.php
  38. +26
    -0
      app/Http/Controllers/PDFController.php
  39. +247
    -0
      app/Http/Controllers/Web/BaseParam.php
  40. +10
    -0
      app/Http/Controllers/Web/IParam.php
  41. +38
    -0
      app/Http/Controllers/Web/IndexController.php
  42. +14
    -0
      app/Http/Controllers/Web/Rule.php
  43. +93
    -0
      app/Http/Controllers/Web/RuleAnalyzer.php
  44. +13
    -0
      app/Http/Controllers/Web/SortableParam.php
  45. +12
    -0
      app/Http/Controllers/Web/TimestampParam.php
  46. +419
    -0
      app/Http/Controllers/Web/WebController.php
  47. +67
    -0
      app/Http/Kernel.php
  48. +17
    -0
      app/Http/Middleware/Authenticate.php
  49. +17
    -0
      app/Http/Middleware/EncryptCookies.php
  50. +17
    -0
      app/Http/Middleware/PreventRequestsDuringMaintenance.php
  51. +30
    -0
      app/Http/Middleware/RedirectIfAuthenticated.php
  52. +19
    -0
      app/Http/Middleware/TrimStrings.php
  53. +20
    -0
      app/Http/Middleware/TrustHosts.php
  54. +28
    -0
      app/Http/Middleware/TrustProxies.php
  55. +22
    -0
      app/Http/Middleware/ValidateSignature.php
  56. +17
    -0
      app/Http/Middleware/VerifyCsrfToken.php
  57. +22
    -0
      app/Jobs/BaseJob.php
  58. +41
    -0
      app/Jobs/Email/SimpleEmail.php
  59. +42
    -0
      app/Jobs/File/DeleteFile.php
  60. +274
    -0
      app/Kintone/KintoneAccess.php
  61. +162
    -0
      app/Kintone/KintoneRecordQuery.php
  62. +14
    -0
      app/Kintone/KintoneRecordQueryOperator.php
  63. +42
    -0
      app/Kintone/Models/CarRoom.php
  64. +15
    -0
      app/Kintone/Models/Customer.php
  65. +12
    -0
      app/Kintone/Models/FieldType.php
  66. +237
    -0
      app/Kintone/Models/KintoneModel.php
  67. +15
    -0
      app/Kintone/Models/SeasonTicketContract.php
  68. +27
    -0
      app/Kintone/Models/Small.php
  69. +37
    -0
      app/Listeners/Email/EmailSendJobRegister.php
  70. +15
    -0
      app/Listeners/Model/CreatedListener.php
  71. +15
    -0
      app/Listeners/Model/DeletedListener.php
  72. +15
    -0
      app/Listeners/Model/DeletingListener.php
  73. +48
    -0
      app/Listeners/Model/ModelListener.php
  74. +25
    -0
      app/Listeners/Model/UpdatingListener.php
  75. +14
    -0
      app/Middlewares/Now.php
  76. +49
    -0
      app/Models/AppModel.php
  77. +67
    -0
      app/Models/BaseModel.php
  78. +38
    -0
      app/Models/Feature/IModelFeature.php
  79. +39
    -0
      app/Models/HistoryModel.php
  80. +109
    -0
      app/Models/User.php
  81. +12
    -0
      app/Models/UserHistory.php
  82. +48
    -0
      app/Providers/AppServiceProvider.php
  83. +26
    -0
      app/Providers/AuthServiceProvider.php
  84. +19
    -0
      app/Providers/BroadcastServiceProvider.php
  85. +38
    -0
      app/Providers/EventServiceProvider.php
  86. +48
    -0
      app/Providers/RouteServiceProvider.php
  87. +67
    -0
      app/Repositories/BaseRepository.php
  88. +29
    -0
      app/Repositories/BaseRepositoryData.php
  89. +32
    -0
      app/Rules/BaseRule.php
  90. +34
    -0
      app/Rules/Kana.php
  91. +60
    -0
      app/Rules/LoginPassword.php
  92. +33
    -0
      app/Rules/NotIncludeCode.php
  93. +32
    -0
      app/Rules/PhoneNumber.php
  94. +32
    -0
      app/Rules/ZipCode.php
  95. +56
    -0
      app/Util/DBUtil.php
  96. +41
    -0
      app/Util/DateUtil.php
  97. +22
    -0
      app/Util/KeysConverter.php
  98. +171
    -0
      app/Util/MigrationHelper.php
  99. +18
    -0
      app/Util/OptionUtil.php
  100. +41
    -0
      app/Util/RouteHelper.php

+ 18
- 0
.editorconfig Näytä tiedosto

@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2

[docker-compose.yml]
indent_size = 4

+ 63
- 0
.env.example Näytä tiedosto

@@ -0,0 +1,63 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kt_mypage
DB_USERNAME=root
DB_PASSWORD=

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

# キントーン定義
KINTONE_HOST=

KINTONE_APP_CARROOM=

+ 11
- 0
.gitattributes Näytä tiedosto

@@ -0,0 +1,11 @@
* text=auto eol=lf

*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php

/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

+ 20
- 0
.gitignore Näytä tiedosto

@@ -0,0 +1,20 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
_*

+ 66
- 0
README.md Näytä tiedosto

@@ -0,0 +1,66 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>

<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>

## About Laravel

Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:

- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).

Laravel is accessible, powerful, and provides tools required for large, robust applications.

## Learning Laravel

Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.

You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.

If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.

## Laravel Sponsors

We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).

### Premium Partners

- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Cubet Techno Labs](https://cubettech.com)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[Many](https://www.many.co.uk)**
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
- **[DevSquad](https://devsquad.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[OP.GG](https://op.gg)**
- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
- **[Lendio](https://lendio.com)**

## Contributing

Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).

## Code of Conduct

In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).

## Security Vulnerabilities

If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.

## License

The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

+ 10
- 0
app/Codes/EnvironmentName.php Näytä tiedosto

@@ -0,0 +1,10 @@
<?php

namespace App\Codes;

enum EnvironmentName: string
{
case LOCAL = 'local';
case STAGING = 'staging';
case PRODUCTOIN = 'production';
}

+ 11
- 0
app/Codes/HTTPResultCode.php Näytä tiedosto

@@ -0,0 +1,11 @@
<?php

namespace App\Codes;

enum HTTPResultCode: int
{
case SECCESS = 0;
case FAILED = 1;
case UNAUTHORIZED = 2;
case EXCLUSIVE_ERROR = 3;
}

+ 130
- 0
app/Codes/PrefCode.php Näytä tiedosto

@@ -0,0 +1,130 @@
<?php

namespace App\Codes;

/**
* JIS X 0401都道府県コード
*/
enum PrefCode: string
{

case HOKKAIDO = '01';
case AOMORI = '02';
case IWATE = '03';
case MIYAGI = '04';
case AKITA = '05';
case YAMAGATA = '06';
case FUKUSHIMA = '07';
case IBARAGI = '08';
case TOCHIGI = '09';
case GUNMA = '10';
case SAITAMA = '11';
case CHIBA = '12';
case TOKYO = '13';
case KANAGAWA = '14';
case NIGATA = '15';
case TOYAMA = '16';
case ISHIKAWA = '17';
case FUKUI = '18';
case YAMANASHI = '19';
case NAGANO = '20';
case GIFU = '21';
case SHIZUOKA = '22';
case AICHI = '23';
case MIE = '24';
case SHIGA = '25';
case KYOTO = '26';
case OSAKA = '27';
case HYOGO = '28';
case NARA = '29';
case WAKAYAMA = '30';
case TOTTORI = '31';
case SHIMANE = '32';
case OKAYAMA = '33';
case HIROSHIMA = '34';
case YAMAGUCHI = '35';
case TOKUSHIMA = '36';
case KAGAWA = '37';
case EHIME = '38';
case KOCHI = '39';
case FUKUOKA = '40';
case SAGA = '41';
case NAGASAKI = '42';
case KUMAMOTO = '43';
case OITA = '44';
case MIYAZAKI = '45';
case KAGOSHIMA = '46';
case OKINAWA = '47';

static private function getDictionary()
{
return [
self::HOKKAIDO->value => '北海道',
self::AOMORI->value => '青森',
self::IWATE->value => '岩手',
self::MIYAGI->value => '宮城',
self::AKITA->value => '秋田',
self::YAMAGATA->value => '山形',
self::FUKUSHIMA->value => '福島',
self::IBARAGI->value => '茨城',
self::TOCHIGI->value => '栃木',
self::GUNMA->value => '群馬',
self::SAITAMA->value => '埼玉',
self::CHIBA->value => '千葉',
self::TOKYO->value => '東京',
self::KANAGAWA->value => '神奈川',
self::NIGATA->value => '新潟',
self::TOYAMA->value => '富山',
self::ISHIKAWA->value => '石川',
self::FUKUI->value => '福井',
self::YAMANASHI->value => '山梨',
self::NAGANO->value => '長野',
self::GIFU->value => '岐阜',
self::SHIZUOKA->value => '静岡',
self::AICHI->value => '愛知',
self::MIE->value => '三重',
self::SHIGA->value => '滋賀',
self::KYOTO->value => '京都',
self::OSAKA->value => '大阪',
self::HYOGO->value => '兵庫',
self::NARA->value => '奈良',
self::WAKAYAMA->value => '和歌山',
self::TOTTORI->value => '鳥取',
self::SHIMANE->value => '島根',
self::OKAYAMA->value => '岡山',
self::HIROSHIMA->value => '広島',
self::YAMAGUCHI->value => '山口',
self::TOKUSHIMA->value => '徳島',
self::KAGAWA->value => '香川',
self::EHIME->value => '愛媛',
self::KOCHI->value => '高知',
self::FUKUOKA->value => '福岡',
self::SAGA->value => '佐賀',
self::NAGASAKI->value => '長崎',
self::KUMAMOTO->value => '熊本',
self::OITA->value => '大分',
self::MIYAZAKI->value => '宮崎',
self::KAGOSHIMA->value => '鹿児島',
self::OKINAWA->value => '沖縄',
];
}

static public function toArray()
{
$ret = [];
$dic = self::getDictionary();
foreach (self::cases() as $val) {
if (isset($dic[$val->value])) {
$ret[][$val->value] = $dic[$val->value];
}
}
return $ret;
}

static public function getName(?PrefCode $code)
{
if ($code === null) return "";
$dic = self::getDictionary();
return data_get($dic, $code->value, "");
}
}

+ 9
- 0
app/Codes/QueueName.php Näytä tiedosto

@@ -0,0 +1,9 @@
<?php

namespace App\Codes;

enum QueueName: string
{
case EMAIL = 'email';
case JOB = 'job';
}

+ 12
- 0
app/Codes/TaxRate.php Näytä tiedosto

@@ -0,0 +1,12 @@
<?php

namespace App\Codes;

/**
* 消費税率
*/
enum TaxRate: int
{
case DEFAULT = 10;
case REDUCED = 8;
}

+ 12
- 0
app/Codes/UserRole.php Näytä tiedosto

@@ -0,0 +1,12 @@
<?php

namespace App\Codes;

/**
* ユーザー権限
*/
enum UserRole: int
{
case NONE = 0;
case NORMAL = 100;
}

+ 105
- 0
app/Console/Commands/BaseCommand.php Näytä tiedosto

@@ -0,0 +1,105 @@
<?php

namespace App\Console\Commands;

use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

abstract class BaseCommand extends Command
{

const RESULTCODE_SUCCESS = 0;
const RESULTCODE_WARN = 1;
const RESULTCODE_FAILED = 2;


/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}


abstract protected function service(): int;
protected bool $outputInfoForBase = true;

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$ret = 0;

$this->boot();
try {
$ret = $this->service();
} catch (Exception $e) {
$message = sprintf("例外発生:%s:%s:%d", $e->getMessage(), $e->getFile(), $e->getLine());
$this->outputError($message, $e->getTrace());
$ret = self::RESULTCODE_FAILED;
}


if ($ret === self::RESULTCODE_SUCCESS) {
$this->outputInfoForBase("成功しました。");
} else if ($ret === self::RESULTCODE_WARN) {
$this->outputWarn("一部失敗があります。");
} else if ($ret === self::RESULTCODE_FAILED) {
$this->outputError("失敗しました");
} else {
$this->outputError(sprintf("未定義のエラーコード:%d", $ret));
}

return $ret;
}

private function boot()
{
Log::setDefaultDriver("batch");
Log::withContext([
'__scheduleId' => strval(Str::uuid()),
...$this->arguments(),
]);
$this->outputInfoForBase(sprintf("バッチ起動 %s", $this->getCommandName()));
}

protected function outputInfo(string $message, array $context = [])
{
Log::info($message, $this->getContext($context));
$this->info($message);
}
private function outputInfoForBase(string $message, array $context = [])
{
if ($this->outputInfoForBase) {
Log::info($message, $this->getContext($context));
}
$this->info($message);
}
protected function outputWarn(string $message, array $context = [])
{
Log::warning($message, $this->getContext($context));
$this->warn($message);
}
protected function outputError(string $message, array $context = [])
{
Log::error($message, $this->getContext($context));
$this->error($message);
}
private function getContext(array $context = [])
{
return array_merge($context, ["context" => $this->arguments()]);
}

protected function getCommandName(): string
{
return "";
}
}

+ 60
- 0
app/Console/Commands/HeartBeat.php Näytä tiedosto

@@ -0,0 +1,60 @@
<?php

namespace App\Console\Commands;


class HeartBeat extends BaseCommand
{

const COMMAND = "heartbeat {--maintenance}";

protected bool $outputInfoForBase = false;

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = self::COMMAND;

/**
* The console command description.
*
* @var string
*/
protected $description = 'ハートビート';

static public function getCommand()
{
return self::COMMAND;
}


/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return int
*/
public function service(): int
{
if ($this->option('maintenance')) {
$isMaintenanceMode = app()->isDownForMaintenance();
if ($isMaintenanceMode) {
$this->outputWarn("down for maintenance");
}
} else {
$this->outputInfo("heart beat");
}
return self::RESULTCODE_SUCCESS;
}
}

+ 210
- 0
app/Console/Commands/RouteListCsv.php Näytä tiedosto

@@ -0,0 +1,210 @@
<?php

namespace App\Console\Commands;

use App\Codes\UserRole;
use App\Http\Controllers\Web\WebController;
use Exception;
use Illuminate\Routing\Route as RoutingRoute;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use LogicException;

class RouteListCsv extends BaseCommand
{

/**
* コントローラー説明の個別定義
* 主にミドルウェア側のコントローラーに適用
*/
const CONTROLLER_DESCRIPTION = [
\Laravel\Sanctum\Http\Controllers\CsrfCookieController::class => "csrf対策用",
];

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'route:csv {--tsv}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'ルート一覧をcsv標準出力する';

private Context $context;

private $fp = null;

private string $separator;

private int $rowIndex = 0;

/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
$this->outputInfoForBase = false;
parent::__construct();
}

/**
* Execute the console command.
*
* @return int
*/
protected function service(): int
{
$routes = Route::getRoutes();
$routes->getRoutes();

$this->separator = $this->getSeparator();

try {
$this->fp = fopen(base_path("routes.csv"), "w");
$this->putHeader();

foreach ($routes->getRoutes() as $r) {

if (Str::startsWith($r->uri(), ["_", "clockwork"])) {
continue;
}


$this->put($r);
}
} catch (Exception $e) {

$this->outputError($e->getMessage());
if ($this->fp !== null) {
fclose($this->fp);
$this->fp = null;
}

return self::RESULTCODE_FAILED;
}
fclose($this->fp);

return self::RESULTCODE_SUCCESS;
}

private function putHeader()
{
fputcsv($this->fp, [
"#",
"NAME",
"説明",
"メソッド",
"URI",
"S",
"C",
"A",
"L",
"N",
"コントローラー",
], $this->separator);
}

private function put(RoutingRoute $route)
{
if ($this->fp === null) {
throw new LogicException("FP null");
}

$controller = $route->getController();


fputcsv($this->fp, [
$this->rowIndex,
$this->getControllerName($controller),
$this->getControllerDescription($controller),
$this->getMethods($route),
$route->uri(),
$this->getRoleAuth($controller, UserRole::NORMAL_ADMIN),
$this->getRoleAuth($controller, UserRole::CONTRACT_ADMIN),
$this->getRoleAuth($controller, UserRole::SUPER_ADMIN),
$controller::class,
], $this->separator);



$this->rowIndex++;
}
private function getMethods(RoutingRoute $route)
{
return implode('|', $route->methods());
}

private function getRoleAuth($controller, UserRole $role): string
{
$ret = true;
if ($controller instanceof WebController) {
$ret = $controller->canAccess($role);
}
return $ret ? "〇" : "-";
}

private function getControllerName($controller): string
{
if ($controller instanceof WebController) {
return $controller->name();
} else if (
method_exists($controller, "name") &&
is_callable([$controller, "name"])
) {
$ret = $controller->name();
return is_string($ret) ? $ret : "-";
} else {

// コントローラ名から名前空間、メソッド名、等を除去
return Str::of($controller::class)
->afterLast("\\")
->beforeLast("@")
->replace("Controller", "");
}
}

private function getControllerDescription($controller): string
{
if ($controller instanceof WebController) {
return $controller->description();
} else if (
method_exists($controller, "description") &&
is_callable([$controller, "description"])
) {
$ret = $controller->description();
return is_string($ret) ? $ret : "-";
} else if (Arr::has(self::CONTROLLER_DESCRIPTION, $controller::class)) {
return self::CONTROLLER_DESCRIPTION[$controller::class];
} else {
return "-";
}
}

private function outputSjisFile()
{

$contents = file_get_contents(base_path("routes.csv"));

file_put_contents(
base_path("routes_sjis.csv"),
mb_convert_encoding(
$contents,
"SJIS",
"UTF8"
)
);
}

private function getSeparator(): string
{
return $this->option("tsv") ? "\t" : ",";
}
}

+ 27
- 0
app/Console/Kernel.php Näytä tiedosto

@@ -0,0 +1,27 @@
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
Schedules\HeartBeat::register($schedule);
}

/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');

require base_path('routes/console.php');
}
}

+ 10
- 0
app/Console/Schedules/BaseSchedule.php Näytä tiedosto

@@ -0,0 +1,10 @@
<?php

namespace App\Console\Schedules;

use Illuminate\Console\Scheduling\Schedule;

abstract class BaseSchedule
{
abstract static public function register(Schedule $schedule);
}

+ 22
- 0
app/Console/Schedules/HeartBeat.php Näytä tiedosto

@@ -0,0 +1,22 @@
<?php

namespace App\Console\Schedules;

use App\Console\Commands\HeartBeat as CommandsHeartBeat;
use Illuminate\Console\Scheduling\Schedule;

class HeartBeat extends BaseSchedule
{

static public function register(Schedule $schedule)
{
$schedule->command(CommandsHeartBeat::class)
->everyFiveMinutes()
->evenInMaintenanceMode()
->description("ハートビート");
$schedule->command(CommandsHeartBeat::class, ['--maintenance'])
->everyMinute()
->evenInMaintenanceMode()
->description("メンテナンスモード確認");
}
}

+ 211
- 0
app/Email/BaseEmailer.php Näytä tiedosto

@@ -0,0 +1,211 @@
<?php

namespace App\Email;

use App\Exceptions\AppCommonException;
use App\Exceptions\TempFileNotExistsException;
use App\Models\Email;
use App\Models\EmailAttachment;
use App\Models\User;
use App\Util\DateUtil;
use Illuminate\Bus\Queueable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

abstract class BaseEmailer extends Mailable
{
use Queueable, SerializesModels;

protected $configRoot = 'mail.mailers.smtp.';

protected $casts = [];

protected User|null $__user = null;
protected string $__email = "";
protected string|null $__userId = null;
protected string|null $__contractId = null;
protected string|null $__receiptIssuingOrderId = null;

/**
* 添付ファイル
* @var Collection<int, EmailAttachment>|null
*/
protected ?Collection $__attachments = null;

public function sendEmail(string $email)
{
$this->__email = $email;
Mail::to($email)
->send($this);
}

public function setUser(User $user)
{
$this->__userId = $user->id;
$this->__email = $user->email;
return $this;
}

public function setEmail(string $target)
{
$this->__email = $target;
return $this;
}

public function setUserId(string $userId)
{
$this->__userId = $userId;
return $this;
}

public function setContractId(string $contractId)
{
$this->__contractId = $contractId;
return $this;
}

public function setReceiptIssuingOrderId(string $receiptIssuingOrderId)
{
$this->__receiptIssuingOrderId = $receiptIssuingOrderId;
return $this;
}

public function build()
{
$this->text($this->getTemplateName())
->subject($this->getSubject())
->with($this->getParams());

// 添付ファイル処理
if ($this->__attachments !== null) {
$count = $this->__attachments->count();
foreach ($this->__attachments as $attachment) {



$filepath = $attachment->filepath;


if (!file_exists($filepath)) {
$e = new TempFileNotExistsException();
throw $e->setFilepath($filepath);
}

$as = $attachment->send_filename;
$mime = $attachment->mime;
$this->attach($filepath, [
'as' => $as,
'mime' => $mime,
]);
}
}

return $this;
}

public function setConfigDefault()
{
$root = $this->configRoot;
config([
$root . "transport" => "smtp",
$root . "host" => env('MAIL_HOST', 'smtp.mailgun.org'),
$root . 'port' => env('MAIL_PORT', 587),
$root . 'encryption' => env('MAIL_ENCRYPTION', 'tls'),
$root . 'username' => env('MAIL_USERNAME'),
$root . 'password' => env('MAIL_PASSWORD'),
$root . 'timeout' => null,
$root . 'auth_mode' => null,
$root . 'verify_peer' => false,
]);
}

public function setConfig(array $config)
{
$root = $this->configRoot;
$ret = [];
foreach ($config as $key => $val) {
$ret[$root . $key] = $val;
}
config($ret);
}

protected function setValues(\stdClass|array $data)
{
foreach ($data as $key => $val) {
$this->setValue($key, $val);
}
}

protected function setValue(string $key, $value)
{
$val = $value;
$camel = Str::camel($key);
$snake = Str::snake($key);

$property = "";

if (property_exists($this, $camel)) {
$property = $camel;
} else if (property_exists($this, $snake)) {
$property = $snake;
} else {
return;
}

if (data_get($this->casts, $property) === 'datetime') {
$this->$property = DateUtil::parse($val);
} else {
$this->$property = $val;
}
}

public function makeModel(): Email
{
if ($this->__email === "") {
throw new AppCommonException("Email宛先不明");
}

$model = new Email();
$model->subject = $this->getSubject();
$model->content = $this->render();
$model->type = get_class($this);
$model->email = $this->__email;

$model->contract_id = $this->__contractId;
$model->user_id = $this->__userId;
$model->receipt_issuing_order_id = $this->__receiptIssuingOrderId;

return $model;
}

abstract public function getTemplateName(): string;

abstract public function getSubject(): string;

abstract public function getParams(): array;


/**
* 画面のURLを生成する
*
* @param array|string $path
* @return string
*/
protected function getAppUrl(array|string $path): string
{
$elements = [config("app.url")];
if (is_array($path)) {
$elements = array_merge($elements, $path);
} else {
$elements[] = $path;
}

return implode(
"/",
$elements,
);
}
}

+ 15
- 0
app/Email/Guests/Guest.php Näytä tiedosto

@@ -0,0 +1,15 @@
<?php

namespace App\Email\Guests;

use App\Email\BaseEmailer;

abstract class Guest extends BaseEmailer
{
public function getParams(): array
{
return array_merge($this->getGuestParams(), []);
}

abstract public function getGuestParams(): array;
}

+ 15
- 0
app/Email/Members/Members.php Näytä tiedosto

@@ -0,0 +1,15 @@
<?php

namespace App\Email\Members;

use App\Email\BaseEmailer;

abstract class Members extends BaseEmailer
{
public function getParams(): array
{
return array_merge($this->getGuestParams(), []);
}

abstract public function getMemberParams(): array;
}

+ 42
- 0
app/Email/Sender.php Näytä tiedosto

@@ -0,0 +1,42 @@
<?php

namespace App\Email;

use App\Models\Email;
use App\Util\DateUtil;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;

class Sender
{

public static function send(string $emailId)
{
$email = Email::findOrFail($emailId);

info("メール送信", [
'id' => $email->id,
'email' => $email->email,
'mailer' => $email->type,
]);

try {
Mail::to($email->email)
->send(new TextEmail($email->subject, $email->content, $email->emailAttachments));
} catch (Exception $e) {
Log::error("メール送信失敗", [
'id' => $email->id,
'email' => $email->email,
'mailer' => $email->type,
]);
$email->is_failed = true;
$email->save();
throw $e;
}


$email->send_datetime = DateUtil::now();
$email->save();
}
}

+ 21
- 0
app/Email/Test.php Näytä tiedosto

@@ -0,0 +1,21 @@
<?php

namespace App\Email;

class Test extends BaseEmailer
{
public function getTemplateName(): string
{
return 'emails.test';
}

public function getSubject(): string
{
return 'メールテスト';
}

public function getParams(): array
{
return [];
}
}

+ 40
- 0
app/Email/TextEmail.php Näytä tiedosto

@@ -0,0 +1,40 @@
<?php

namespace App\Email;

use Illuminate\Bus\Queueable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Queue\SerializesModels;

class TextEmail extends BaseEmailer
{
use Queueable, SerializesModels;

private string $__subject;
private string $__contents;


public function __construct(string $subject, string $contents, ?Collection $attachments = null)
{
$this->__subject = $subject;
$this->__contents = $contents;
$this->__attachments = $attachments;
}

public function getTemplateName(): string
{
return 'emails.free_text';
}

public function getSubject(): string
{
return $this->__subject;
}

public function getParams(): array
{
return [
'contents' => $this->__contents,
];
}
}

+ 42
- 0
app/Events/Email/ConfirmEvent.php Näytä tiedosto

@@ -0,0 +1,42 @@
<?php

namespace App\Events\Email;

use App\Email\BaseEmailer;
use App\Models\Email;
use App\Models\EmailAttachment;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ConfirmEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public Email $email;

/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Email|BaseEmailer $email, ?Collection $attachments = null)
{
if ($email instanceof Email) {
$this->email = $email;
} else {
$this->email = $email->makeModel();
$this->email->save();
}
if ($attachments !== null) {
foreach ($attachments as $attachment) {
if (!($attachment instanceof EmailAttachment)) continue;

$emailId = $this->email->id;
$attachment->email_id = $emailId;
$attachment->save();
}
}
}
}

+ 8
- 0
app/Events/Model/CreatedEvent.php Näytä tiedosto

@@ -0,0 +1,8 @@
<?php

namespace App\Events\Model;


class CreatedEvent extends ModelChangeEvent
{
}

+ 8
- 0
app/Events/Model/DeletedEvent.php Näytä tiedosto

@@ -0,0 +1,8 @@
<?php

namespace App\Events\Model;


class DeletedEvent extends ModelChangeEvent
{
}

+ 8
- 0
app/Events/Model/DeletingEvent.php Näytä tiedosto

@@ -0,0 +1,8 @@
<?php

namespace App\Events\Model;


class DeletingEvent extends ModelChangeEvent
{
}

+ 36
- 0
app/Events/Model/ModelChangeEvent.php Näytä tiedosto

@@ -0,0 +1,36 @@
<?php

namespace App\Events\Model;

use App\Models\Feature\IModelFeature;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

abstract class ModelChangeEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public IModelFeature $model;

/**
* Create a new event instance.
*/
public function __construct(IModelFeature $model)
{
$this->model = $model;
}

/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}

+ 8
- 0
app/Events/Model/UpdatingEvent.php Näytä tiedosto

@@ -0,0 +1,8 @@
<?php

namespace App\Events\Model;


class UpdatingEvent extends ModelChangeEvent
{
}

+ 9
- 0
app/Exceptions/AppCommonException.php Näytä tiedosto

@@ -0,0 +1,9 @@
<?php

namespace App\Exceptions;

use Exception;

class AppCommonException extends Exception
{
}

+ 13
- 0
app/Exceptions/ConfigException.php Näytä tiedosto

@@ -0,0 +1,13 @@
<?php

namespace App\Exceptions;

use Exception;

class ConfigException extends Exception
{
public function __construct(string $key, $value)
{
parent::__construct("設定エラー: key:" . $key . " value:" . $value);
}
}

+ 12
- 0
app/Exceptions/ExclusiveException.php Näytä tiedosto

@@ -0,0 +1,12 @@
<?php

namespace App\Exceptions;

use Exception;

/**
* 排他エラー
*/
class ExclusiveException extends Exception
{
}

+ 9
- 0
app/Exceptions/GeneralErrorMessageException.php Näytä tiedosto

@@ -0,0 +1,9 @@
<?php

namespace App\Exceptions;

use Exception;

class GeneralErrorMessageException extends Exception
{
}

+ 48
- 0
app/Exceptions/Handler.php Näytä tiedosto

@@ -0,0 +1,48 @@
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];

/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];

/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];

/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}

+ 21
- 0
app/Exceptions/TempFileNotExistsException.php Näytä tiedosto

@@ -0,0 +1,21 @@
<?php

namespace App\Exceptions;

use Exception;

class TempFileNotExistsException extends Exception
{
private $filepath = "";

public function setFilepath(string $filepath): static
{
$this->filepath = $filepath;
return $this;
}

public function getFilepath(): string
{
return $this->filepath;
}
}

+ 14
- 0
app/Features/InstanceAble.php Näytä tiedosto

@@ -0,0 +1,14 @@
<?php

namespace App\Features;

trait InstanceAble
{
/**
* @return static
*/
public static function instance()
{
return app()->make(self::class);
}
}

+ 12
- 0
app/Http/Controllers/Controller.php Näytä tiedosto

@@ -0,0 +1,12 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}

+ 26
- 0
app/Http/Controllers/PDFController.php Näytä tiedosto

@@ -0,0 +1,26 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use PDF;

class PDFController extends Controller
{
public function index()
{

// https://wkhtmltopdf.org/usage/wkhtmltopdf.txt


// $pdf = PDF::loadHTML('<h1>Hello World 岩渕</h1>');
$pdf = PDF::loadView('pdf');


// はがきサイズを指定
$pdf->setOption('page-height', 148)
->setOption('page-width', 100)
->setOption('encoding', 'utf-8');
return $pdf->inline();
}
}

+ 247
- 0
app/Http/Controllers/Web/BaseParam.php Näytä tiedosto

@@ -0,0 +1,247 @@
<?php

namespace App\Http\Controllers\Web;

use App\Repositories\BaseRepository;
use App\Util\DateUtil;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Enum;
use ReflectionClass;

abstract class BaseParam implements IParam
{
const REQUIRED = 'required';
const NULLABLE = 'nullable';
const STR = 'string';
const NUMERIC = 'numeric';
const DATE = 'date';
const BOOLEAN_ = 'boolean';
const IMAGE = 'image';
const FILE = 'file';
const ARRAY = 'array';

const PARAM_NAME_TIMESTAMP = 'timestamp';

private array $param = [];

abstract public function rules(): array;

public function __set($name, $value)
{
$name = Str::snake($name);
$rule = data_get($this->rules(), $name, null);
if (!$rule) {
throw new Exception('存在しないパラメータ ' . $name);
}

$this->param[$name] = $value;
}

public function __get($name)
{
return data_get($this->param, Str::snake($name), null);
}

public function toArray(bool $toCamelCase = false): array
{
if ($toCamelCase === false) {
return $this->param;
}

$ret = [];
foreach ($this->param as $key => $val) {
$camelKey = Str::camel($key);
$ret[$camelKey] = $val;
}
return $ret;
}

public function setData(array $data)
{

$dots = Arr::dot($data);

$ruleRegExs = RuleAnalyzer::convertToRegEx($this->rules());

// logger($ruleRegExs);

foreach ($dots as $name => $value) {

if ($value === null) {
data_set($this->param, $name, null);
continue;
}

$analyzer = new RuleAnalyzer($name, $ruleRegExs);

if ($analyzer->isMathed()) {
$content = $this->getSettableData($analyzer->getType(), $value);
data_set($this->param, $name, $content);
}
}

// logger($this->param);
}

private function getSettableData($rule, $value)
{
if (is_string($rule)) {
if ($rule === self::STR) {
return strval($value);
}
if ($rule === self::NUMERIC) {
return intval($value);
}
if ($rule === self::BOOLEAN_) {
return boolval($value);
}
if ($rule === self::DATE) {
if (is_string($value)) {
return DateUtil::parse($value);
} else {
return null;
}
}
if ($rule === self::IMAGE || $rule === self::FILE) {
return $value;
}
if ($rule === self::ARRAY) {
return $value;
}
} elseif ($rule instanceof Enum) {
// リフレクションを使ってEnumの型を取得する
$ref = new ReflectionClass((get_class($rule)));
$type = $ref->getProperty('type');
$type->setAccessible(true);
$enum = $type->getValue($rule);
try {
return $enum::tryFrom($value);
} catch (Exception $e) {
logs()->error('Enum パース失敗', ['rule' => $rule, 'value' => $value, 'exception' => $e->getMessage()]);
throw $e;
}
}


throw new Exception(sprintf("不正な変換 ",));
}

/**
* 排他チェック
*
* @param Carbon|null $timestamp
* @return boolean
*/
public function checkTimestamp(Carbon|null $timestamp): bool
{
if ($timestamp === null) return true;

$param = $this->__get(self::PARAM_NAME_TIMESTAMP);
if ($param === null || !$param instanceof Carbon) {
logger("無効なタイムスタンプ確認");
logger($param);
return false;
}

return $param->eq($timestamp);
}

private function isNullable(array|bool $condition, bool $nullable): bool
{
if (is_array($condition)) {
return $nullable;
} else {
return $condition;
}
}

protected function str(array|bool $condition = [], $nullable = false): array
{
$conditionEx = array_merge(is_array($condition) ? $condition : [], ['max:250']);

return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::STR
], $conditionEx);
}
protected function text(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::STR
], is_array($condition) ? $condition : []);
}
protected function numeric(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::NUMERIC
], is_array($condition) ? $condition : []);
}
protected function boolean(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::BOOLEAN_
], is_array($condition) ? $condition : []);
}
protected function array(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::ARRAY
], is_array($condition) ? $condition : []);
}
protected function date(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::DATE
], is_array($condition) ? $condition : []);
}
protected function enum(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
], is_array($condition) ? $condition : []);
}
protected function image(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::IMAGE
], is_array($condition) ? $condition : []);
}
protected function images(string $name, $nullable = false): array
{
$need = $nullable ? self::NULLABLE : self::REQUIRED;
return [
$name => [$need, self::ARRAY],
sprintf("%s.*", $name) => [$need, self::IMAGE]
];
}
protected function file(array|bool $condition = [], $nullable = false): array
{
return array_merge([
$this->isNullable($condition, $nullable) ? self::NULLABLE : self::REQUIRED,
self::FILE
], is_array($condition) ? $condition : []);
}

protected function sortableRules()
{
return [
BaseRepository::CONDITION_SORT_TARGET => $this->str(true),
BaseRepository::CONDITION_SORT_ORDER => $this->str(true),
BaseRepository::CONDITION_LIMIT => $this->numeric(true),
];
}

protected function timestamp(bool $nullable = false)
{
return [self::PARAM_NAME_TIMESTAMP => $this->date($nullable)];
}
}

+ 10
- 0
app/Http/Controllers/Web/IParam.php Näytä tiedosto

@@ -0,0 +1,10 @@
<?php

namespace App\Http\Controllers\Web;

interface IParam
{
public function setData(array $data);
public function rules(): array;
public function toArray(): array;
}

+ 38
- 0
app/Http/Controllers/Web/IndexController.php Näytä tiedosto

@@ -0,0 +1,38 @@
<?php

namespace App\Http\Controllers\Web;

use Illuminate\Routing\Controller as BaseController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class IndexController extends BaseController
{

public function description(): string
{
return "viewの返却";
}

public function entry(Request $request)
{
if (!$request->expectsJson()) {
return response()->view('index')->withHeaders($this->makeHeader());
} else {
return response()->json([], 404);
}
}

private function makeHeader(): array
{
$header = [];
$user = Auth::user();
if ($user) {
// $header["User-Auth"] = sprintf("%d,%d", $user->id, $user->role->value);
$header["User-Auth"] = "yes";
} else {
$header["User-Auth"] = 'none';
}
return $header;
}
}

+ 14
- 0
app/Http/Controllers/Web/Rule.php Näytä tiedosto

@@ -0,0 +1,14 @@
<?php

namespace App\Http\Controllers\Web;

abstract class Rule
{
public static function email(): array
{
$ret = [];
$ret[] = "email:strict,filter,dns";
$ret[] = "max:255";
return $ret;
}
}

+ 93
- 0
app/Http/Controllers/Web/RuleAnalyzer.php Näytä tiedosto

@@ -0,0 +1,93 @@
<?php

namespace App\Http\Controllers\Web;

use Illuminate\Support\Str;

class RuleAnalyzer
{

static public function convertToRegEx(array $rules)
{
$ret = [];
foreach ($rules as $name => $ruleList) {
$pattern = '/^' . Str::replace('*', '\d+', Str::replace('.', '\.', $name)) . '$/';
$ret[$pattern] = $ruleList;
}
return $ret;
}

private string $path;
private string $pattern;

private int|null $arrayIndex = null;

private bool $mathed = false;

private array $rules;

public function __construct(string $path, array &$rules)
{
$this->path = $path;

// パターンマッチング
foreach ($rules as $pattern => $ruleList) {
if (preg_match($pattern, $path, $matcheds)) {
$this->pattern = $pattern;
$this->rules = $ruleList;
$this->mathed = true;
break;
}
}

if (!$this->mathed) {
return;
}

// 配列インデックスの取得
$this->arrayIndex = $this->getArrayIndexFromPath($path);
}

public function isMathed()
{
return $this->mathed;
}

public function getRules()
{
return $this->rules;
}

public function getType()
{
return $this->rules[1];
}

public function isArrayMember()
{
return $this->arrayIndex !== null;
}

public function getArrayIndex()
{
return $this->arrayIndex;
}

private function getArrayIndexFromPath(string $path)
{

preg_match('/^.+\.(\d+)\.[0-9A-Za-z_]+$/', $path, $matches);
if (count($matches) === 0) {
return null;
}

return intval($matches[1]);
}

public function getArrayName()
{
$list = explode('.*.', $this->pattern);
array_pop($list);
return implode('.*.');
}
}

+ 13
- 0
app/Http/Controllers/Web/SortableParam.php Näytä tiedosto

@@ -0,0 +1,13 @@
<?php

namespace App\Http\Controllers\Web;


/**
* @property ?string $sort
* @property ?string $order
* @property ?int $limit
*/
interface SortableParam
{
}

+ 12
- 0
app/Http/Controllers/Web/TimestampParam.php Näytä tiedosto

@@ -0,0 +1,12 @@
<?php

namespace App\Http\Controllers\Web;

use Illuminate\Support\Carbon;

/**
* @property ?Carbon $timestamp
*/
interface TimestampParam
{
}

+ 419
- 0
app/Http/Controllers/Web/WebController.php Näytä tiedosto

@@ -0,0 +1,419 @@
<?php

namespace App\Http\Controllers\Web;

use App\Codes\HTTPResultCode as ResultCode;
use App\Codes\UserRole;
use App\Exceptions\AppCommonException;
use App\Exceptions\ExclusiveException;
use App\Exceptions\GeneralErrorMessageException;
use App\Util\DBUtil;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use LogicException;
use Slack;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;

abstract class WebController extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;

const COL_NAME_CREATED_AT = 'created_at';
const COL_NAME_UPDATED_AT = 'updated_at';

const COL_NAME_RESULT_CODE = 'result';
const COL_NAME_DATA = 'data';
const COL_NAME_MESSAGES = 'messages';
const COL_NAME_GENERAL_MESSAGE = 'general';
const COL_NAME_EMAIL_ID = 'email_id';
const COL_NAME_ERRORS = 'errors';

/**
* バリデートした結果を格納
*
* @var array
*/
protected $validated = [];

/**
* 画面へ返却するメールID
*
* @var integer|null
*/
private int|null $emailId = null;

/**
* 返却するメッセージ
*
* @var array|null
*/
private array|null $messages = null;

/**
* 返却するメッセージ
*
* @var string|null
*/
private string|null $generalMessage = null;

/**
* 返却するデータ
*
* @var mixed|null
*/
private $data = null;

protected DBUtil $transaction;

/**
* 返却する結果コード
*
* @var ResultCode|null
*/
private ResultCode|null $resultCode = ResultCode::SECCESS;

public function __construct()
{
$this->transaction = DBUtil::instance();
}


/**
* パラメータオブジェクト
*/
protected function getParam(): IParam
{
if (!property_exists(static::class, 'param')) {
throw new LogicException("param未定義");
}

$param = $this->param;

if (!is_subclass_of($param, IParam::class)) {
throw new LogicException("param型不正");
}
return $this->param;
}


/**
* コントローラーの名前
* オーバーライドされることを想定
* 主に、Routeのドキュメント作成用
*
* @return string
*/
public function name(): string
{
return "---未設定---";
}

/**
* コントローラーの説明
* オーバーライドされることを想定
* 主に、Routeのドキュメント作成用
*
* @return string
*/
public function description(): string
{
return "---未設定---";
}

/**
* オーバーライド必要
* メインロジック
*
* @param Request $request
* @return Response|JsonResponse|string
*/
protected function run(Request $request): Response|JsonResponse|BinaryFileResponse|string
{
return $this->successResponse();
}

private function getRules()
{
return $this->getParam()->rules();
}

public function entry(Request $request)
{
$this->setLogContext($request);

try {
$validator = Validator::make($request->all(), $this->getRules());
$validator->validate();
} catch (ValidationException $e) {
logger("validate error", ['errors' => $e->errors(), 'request' => $request->all(), 'path' => $request->path()]);
return $this->validateErrorResponse($e);
}

try {
$this->validated = $validator->validated();
$this->getParam()->setData($this->validated);

$this->authorize();

$this->transaction->beginTransaction();
$ret = $this->run($request);

$this->transaction->commit();
Slack::commit();
return $ret;
} catch (GeneralErrorMessageException $e) {
$this->transaction->rollBack();
Slack::commit();
return $this->failedResponse([], $e->getMessage());
} catch (AppCommonException $e) {
$this->transaction->rollBack();
Slack::commit();
logs()->error(sprintf("Appエラー:%s File:%s Line:%d", $e->getMessage(), $e->getFile(), $e->getLine()));
return $this->failedResponse();
} catch (ExclusiveException $e) {
$this->transaction->rollBack();
Slack::commit();
logs()->error(sprintf("排他エラー:%s", $e->getMessage()));
return $this->exclusiveErrorResponse();
} catch (LogicException $e) {
$this->transaction->rollBack();
Slack::commit();
logs()->error([
sprintf("実装エラー:%s", $e->getMessage()),
get_class($e),
$e->getFile(),
$e->getLine(),
$request->all(),
]);
logger(array_filter($e->getTrace(), function ($val, $key) {
return $key <= 5;
}, ARRAY_FILTER_USE_BOTH));
return $this->failedResponse();
} catch (ValidationException $e) {
$this->transaction->rollBack();
Slack::commit();
return $this->validateErrorResponse($e);
} catch (HttpException $e) {
$this->transaction->rollBack();
Slack::commit();
if ($e->getStatusCode() === 401) {
return $this->unAuthorizedResponse();
}
throw e;
} catch (Exception $e) {
$this->transaction->rollBack();
Slack::commit();
logs()->error([
sprintf("例外エラー:%s", $e->getMessage()),
get_class($e),
$e->getFile(),
$e->getLine(),
$request->all(),
]);
logger(array_filter($e->getTrace(), function ($val, $key) {
return $key <= 5;
}, ARRAY_FILTER_USE_BOTH));
return $this->failedResponse();
}
}

protected function successResponse(array|object $data = [], array|string $messages = [])
{
return $this->setData($data)
->setMessages($messages)
->setResultCode(ResultCode::SECCESS)
->makeResponse();
}

protected function failedResponse(array|object $data = [], array|string $messages = [])
{
return $this->setData($data)
->setMessages($messages)
->setResultCode(ResultCode::FAILED)
->makeResponse();
}
protected function unAuthorizedResponse(array|object $data = [], array|string $messages = [])
{
return $this->setData($data)
->setMessages($messages)
->setResultCode(ResultCode::UNAUTHORIZED)
->makeResponse();
}
protected function exclusiveErrorResponse(array|object $data = [], array|string $messages = [])
{
return $this->setData($data)
->setMessages($messages)
->setResultCode(ResultCode::EXCLUSIVE_ERROR)
->makeResponse();
}

protected function validateErrorResponse(ValidationException|array $exception, string|null $generalMessage = null)
{

$errorMessages = [];
$general = null;
if ($exception instanceof ValidationException) {
foreach ($exception->errors() as $key => $m) {
$errorMessages[$key] = $m[0];
}
}

if (is_array($exception)) {
$errorMessages = $exception;
}

$general = $generalMessage ?? data_get($errorMessages, self::COL_NAME_GENERAL_MESSAGE);

return $this->setData([])
->setMessages($errorMessages)
->setGeneralMessage($general)
->setResultCode(ResultCode::FAILED)
->makeResponse();
}

private function makeResponse()
{
if ($this->resultCode === null) {
abort(403);
}

$ret = [];
Arr::set($ret, self::COL_NAME_RESULT_CODE, $this->resultCode->value);
if ($this->data !== null) {
Arr::set($ret, self::COL_NAME_DATA, $this->data);
}
if ($this->messages !== null) {
Arr::set($ret, self::COL_NAME_MESSAGES . "." . self::COL_NAME_ERRORS, $this->messages);
}
if ($this->generalMessage !== null) {
Arr::set($ret, self::COL_NAME_MESSAGES . "." . self::COL_NAME_GENERAL_MESSAGE, $this->generalMessage);
}
if ($this->emailId !== null) {
Arr::set($ret, self::COL_NAME_MESSAGES . "." . self::COL_NAME_EMAIL_ID, $this->emailId);
}

if (request()->wantsJson()) {
return response()
->json($ret)
->withHeaders($this->makeHeader());
} else {
abort(500);
}
}

private function makeHeader(): array
{
$header = [];
$user = Auth::user();
if ($user) {
$header["App-User-Auth"] = sprintf("%d,%d", $user->id, $user->role->value);
} else {
$header["App-User-Auth"] = 'none';
}
return $header;
}

// 以下 認可関係
protected array|null $roleAllow = null;
protected array|null $roleDisallow = null;
protected array|null $customAllow = null;

protected function roleAllow(UserRole $role)
{
$this->roleAllow = [];
foreach (UserRole::cases() as $ele) {
if ($role->value <= $ele->value) {
$this->roleAllow[] = $ele;
}
}
}

private function authorize()
{
if (!Auth::check()) {
return;
}

$role = Auth::user()->role;

if (!$this->canAccess($role)) {
abort(401);
}
}

public function canAccess(UserRole $role)
{
if (is_array($this->roleAllow) && !in_array($role, $this->roleAllow)) {
return false;
}

if (is_array($this->roleDisallow) && in_array($role, $this->roleDisallow)) {
return false;
}

return $this->canCustomAccess();
}

public function canCustomAccess(): bool
{

return true;
}

// 返却用データの登録
protected function setEmailId(int $emailId)
{
$this->emailId = $emailId;
return $this;
}

protected function setMessages(array|string $messages)
{
if (is_array($messages)) {
$this->messages = $messages;
} else {
$this->setGeneralMessage($messages);
}
return $this;
}

protected function setGeneralMessage(string|null $generalMessage)
{
$this->generalMessage = $generalMessage;
return $this;
}

protected function setData($data)
{
$this->data = $data;
return $this;
}

protected function setResultCode(ResultCode $resultCode)
{
$this->resultCode = $resultCode;
return $this;
}

protected function setLogContext(Request $request)
{
Log::withContext([
'__requestUuid__' => strval(Str::uuid()),
'__userId__' => Auth::id(),
'__path__' => $request->path(),
]);
}
}

+ 67
- 0
app/Http/Kernel.php Näytä tiedosto

@@ -0,0 +1,67 @@
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];

/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'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,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}

+ 17
- 0
app/Http/Middleware/Authenticate.php Näytä tiedosto

@@ -0,0 +1,17 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;

class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

+ 17
- 0
app/Http/Middleware/EncryptCookies.php Näytä tiedosto

@@ -0,0 +1,17 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;

class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

+ 17
- 0
app/Http/Middleware/PreventRequestsDuringMaintenance.php Näytä tiedosto

@@ -0,0 +1,17 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;

class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

+ 30
- 0
app/Http/Middleware/RedirectIfAuthenticated.php Näytä tiedosto

@@ -0,0 +1,30 @@
<?php

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;

foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}

return $next($request);
}
}

+ 19
- 0
app/Http/Middleware/TrimStrings.php Näytä tiedosto

@@ -0,0 +1,19 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;

class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

+ 20
- 0
app/Http/Middleware/TrustHosts.php Näytä tiedosto

@@ -0,0 +1,20 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Http\Middleware\TrustHosts as Middleware;

class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

+ 28
- 0
app/Http/Middleware/TrustProxies.php Näytä tiedosto

@@ -0,0 +1,28 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;

class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;

/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

+ 22
- 0
app/Http/Middleware/ValidateSignature.php Näytä tiedosto

@@ -0,0 +1,22 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Routing\Middleware\ValidateSignature as Middleware;

class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

+ 17
- 0
app/Http/Middleware/VerifyCsrfToken.php Näytä tiedosto

@@ -0,0 +1,17 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

+ 22
- 0
app/Jobs/BaseJob.php Näytä tiedosto

@@ -0,0 +1,22 @@
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

abstract class BaseJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* ジョブを再試行する前に待機する秒数を計算
*/
public function backoff(): int
{
return 5;
}
}

+ 41
- 0
app/Jobs/Email/SimpleEmail.php Näytä tiedosto

@@ -0,0 +1,41 @@
<?php

namespace App\Jobs\Email;

use App\Codes\QueueName;
use App\Email\Sender;
use App\Exceptions\TempFileNotExistsException;
use App\Jobs\BaseJob;
use App\Models\Email;
use Illuminate\Support\Facades\Log;

class SimpleEmail extends BaseJob
{
private string $emailId;

/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Email $email)
{
$this->emailId = $email->id;
$this->onQueue(QueueName::EMAIL->value);
}

/**
* Execute the job.
*
* @return void
*/
public function handle()
{
try {
Sender::send($this->emailId);
} catch (TempFileNotExistsException $e) {
// 一時ファイルが存在しないため、処理終了
Log::warning(sprintf("ファイル存在しないため、メール送信処理スキップ :%s", $e->getFilepath()));
}
}
}

+ 42
- 0
app/Jobs/File/DeleteFile.php Näytä tiedosto

@@ -0,0 +1,42 @@
<?php

namespace App\Jobs\File;

use App\Codes\QueueName;
use App\Files\TmpFile;
use App\Jobs\BaseJob;
use Illuminate\Support\Facades\Storage;

class DeleteFile extends BaseJob
{

private string $fileId;
private string $storagePath;

/**
* Create a new job instance.
*
* @return void
*/
public function __construct(
private TmpFile $file,
) {
$this->onQueue(QueueName::JOB->value);
$this->fileId = $file->getId();
$this->storagePath = $file->getPath();
logger("FILE削除JOB登録:" . $this->storagePath);
}

/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if (Storage::exists($this->storagePath)) {
Storage::delete($this->storagePath);
info(sprintf("ファイル削除:%s ", $this->storagePath));
}
}
}

+ 274
- 0
app/Kintone/KintoneAccess.php Näytä tiedosto

@@ -0,0 +1,274 @@
<?php

namespace App\Kintone;

use App\Exceptions\ConfigException;
use App\Kintone\Models\KintoneModel;
use Exception;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Log;

class KintoneAccess
{

private string $appName;
private string $host;
private string $apiToken;
private int $appId;

private string|null $cursor = null;
private int $cursorDataCount = 0;
private bool $hasNext = false;

private const URL_RECORD = "/k/v1/record.json";
private const URL_CURSOL = "/k/v1/records/cursor.json";


public function __construct(string $appName)
{
$this->appName = $appName;

$key = "kintone.host";
$host = config($key);
if (!$host) {
throw new ConfigException("kintone.host", $host);
}

$key = "kintone.applications." . $appName . ".apiToken";
$apiToken = config($key);
if (!$apiToken) {
throw new ConfigException($key, $apiToken);
}

$key = "kintone.applications." . $appName . ".appId";
$appId = config($key);
if (!$appId) {
throw new ConfigException($key, $appId);
}

$this->host = $host;
$this->apiToken = $apiToken;
$this->appId = $appId;
}

public function __destruct()
{
$this->deleteCursor();
}


public function find(int $id, KintoneModel &$result): Response
{

$response = Http::withHeaders([
"X-Cybozu-API-Token" => $this->apiToken,
])->get($this->getRecordUrl(), [
"app" => $this->appId,
"id" => $id,
]);

if ($response->failed()) {
$e = $response->toException();
if ($e instanceof Exception) {
throw $e;
}
}

$result->setDataFromRecordResponse($response['record']);
return $response;
}

public function update(KintoneModel &$model)
{
$response = Http::withHeaders([
"X-Cybozu-API-Token" => $this->apiToken,
])->put($this->getRecordUrl(), [
"app" => $this->appId,
"id" => $model->getRecordId(),
"record" => $model->getApiLayout(),
"revision" => $model->getRevision(),
]);

if ($response->failed()) {
$e = $response->toException();
if ($e instanceof Exception) {
throw $e;
}
}

$model->clean($response["revision"]);
return $response;
}

public function create(KintoneModel &$model)
{
$response = Http::withHeaders([
"X-Cybozu-API-Token" => $this->apiToken,
])->post($this->getRecordUrl(), [
"app" => $this->appId,
"record" => $model->getApiLayout(),
]);

if ($response->failed()) {
$e = $response->toException();
if ($e instanceof Exception) {
Log::error($e->getMessage());
throw $e;
}
}

$model->clean($response["revision"]);
$model->setRecordId($response["id"]);
return $response;
}

public function createCursor(KintoneRecordQuery|null $query = null, array|null $fields = null)
{
$data = [
"app" => $this->appId,
];
if ($query !== null) {
$data["query"] = $query->toQuery();
}
if ($fields !== null) {
$data["fields"] = $fields;
}

$response = Http::withHeaders([
"X-Cybozu-API-Token" => $this->apiToken,
])->post($this->getCursorUrl(), $data);

if ($response->failed()) {
$e = $response->toException();
if ($e instanceof Exception) {
Log::error($e->getMessage());
throw $e;
}
}

$this->cursor = $response["id"];
$this->cursorDataCount = $response["totalCount"];

if (0 < $this->cursorDataCount) {
$this->hasNext = true;
}

return $response;
}


/**
* @return Collection<KintoneModel>
*/
public function next()
{
if (!$this->hasNext) {
return collect();
}

$response = Http::withHeaders([
"X-Cybozu-API-Token" => $this->apiToken,
])->get($this->getCursorUrl(), [
"id" => $this->cursor,
]);

if ($response->failed()) {
$e = $response->toException();
if ($e instanceof Exception) {
Log::error($e->getMessage());
throw $e;
}
}

$ret = collect();

$hasNext = $response["next"];
if (!$hasNext) {
$this->cursor = null;
$this->hasNext = false;
$this->cursorDataCount = 0;
}

foreach ($response["records"] as $data) {
/**
* @var KintoneModel $model
*/
$model = new $this->appName();

$model->setDataFromRecordResponse($data);
$ret->push($model);
}



return $ret;
}

/**
* @return Collection<KintoneModel>
*/
public function all(KintoneRecordQuery|null $query = null, array|null $fields = null)
{

if ($this->cursor !== null) {
$this->deleteCursor();
}
$this->createCursor($query, $fields);

$list = collect();
while (true) {
$ret = $this->next();

foreach ($ret as $ele) {
$list->push($ele);
}

if ($this->cursor === null) {
break;
}
}
return $list;
}

public function deleteCursor()
{
if ($this->cursor === null) {
return;
}

$response = Http::withHeaders([
"X-Cybozu-API-Token" => $this->apiToken,
])->delete($this->getCursorUrl(), [
"id" => $this->cursor,
]);

if ($response->failed()) {
$e = $response->toException();
if ($e instanceof Exception) {
Log::error($e->getMessage());
throw $e;
}
}

$this->cursor = null;
$this->hasNext = false;
$this->cursorDataCount = 0;
}

private function getRecordUrl()
{
return $this->getUrl(self::URL_RECORD);
}

private function getCursorUrl()
{
return $this->getUrl(self::URL_CURSOL);
}

private function getUrl(string $path)
{
return $this->host . $path;
}
}

+ 162
- 0
app/Kintone/KintoneRecordQuery.php Näytä tiedosto

@@ -0,0 +1,162 @@
<?php

namespace App\Kintone;

use Closure;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

class KintoneRecordQuery
{

private Collection $query;
private string $order = "";
private string $appName;


public function __construct(string $appName)
{
$this->query = collect();
$this->appName = $appName;
}

public function toQuery(): string
{
$ret = "";
foreach ($this->query as $ele) {
$ret .= $ele;
$ret .= " ";
}
$ret .= $this->order;
logger(sprintf("QUERY[%s]:%s", $this->appName, $ret));
return $ret;
}

public function where(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
return $this->whereQuery("and", $operator->value, $column, $condition);
}
public function notWhere(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
return $this->whereQuery("and", $operator->value, $column, $condition);
}

public function orWhere(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
$this->whereQuery("or", $operator->value, $column, $condition);
return $this;
}
public function notOrWhere(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
return $this->whereQuery("or", $operator->value, $column, $condition);
}
public function whereIn(string|Closure $column, array $condition)
{
return $this->whereQuery("and", "in", $column, $condition);
}
public function whereNotIn(string|Closure $column, array $condition)
{
return $this->whereQuery("and", "not in", $column, $condition);
}
public function orWhereIn(string|Closure $column, array $condition)
{
return $this->whereQuery("or", "not in", $column, $condition);
}
public function whereDate(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
return $this->whereQuery("and", $operator->value, $column, $date->format('Y-m-d'));
}
public function whereDateTime(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
return $this->whereQuery("and", $operator->value, $column, $date->toIso8601ZuluString());
}
public function orWhereDate(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
return $this->whereQuery("or", $operator->value, $column, $date->format('Y-m-d'));
}
public function orWhereDateTime(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ)
{
return $this->whereQuery("or", $operator->value, $column, $date->toIso8601ZuluString());
}

public function orderByAsc(string $column)
{
return $this->orderBy($column, "asc");
}
public function orderByDesc(string $column)
{
return $this->orderBy($column, "desc");
}
private function orderBy(string $column, string $order)
{
$this->order = sprintf("order by %s %s", $column, $order);
return $this;
}

private function whereQuery(
string $andOr,
string $operator,
string|Closure $column,
string|int|array|null $condition
) {
if (!$this->query->isEmpty()) {
$this->query->push($andOr);
}

if (is_string($column)) {
if (is_string($condition)) {
$this->query->push(sprintf(
'%s %s "%s"',
$column,
$operator,
$condition,
));
return $this;
}
if (is_int($condition)) {
$this->query->push(sprintf(
'%s %s %d',
$column,
$operator,
$condition,
));
return $this;
}
if (is_array($condition)) {
$in = "";
foreach ($condition as $ele) {
if (is_string($ele)) {
if ($in !== "") {
$in .= ", ";
}
$in .= sprintf('"%s"', $ele);
}
if (is_integer($ele)) {
if ($in !== "") {
$in .= ", ";
}
$in .= sprintf('%d', $ele);
}
}
$this->query->push(sprintf(
'%s %s (%s)',
$column,
$operator,
$in,
));
return $this;
}
}


if ($column instanceof Closure) {
$nestedCondition = new self($this->appName);
$column($nestedCondition);
$this->query->push(sprintf(
'(%s)',
$nestedCondition->toQuery()
));
return $this;
}
}
}

+ 14
- 0
app/Kintone/KintoneRecordQueryOperator.php Näytä tiedosto

@@ -0,0 +1,14 @@
<?php

namespace App\Kintone;


enum KintoneRecordQueryOperator: string
{
case EQ = "=";
case NEQ = "!=";
case GT = ">";
case LT = "<";
case GE = ">=";
case LE = "<=";
}

+ 42
- 0
app/Kintone/Models/CarRoom.php Näytä tiedosto

@@ -0,0 +1,42 @@
<?php

namespace App\Kintone\Models;


class CarRoom extends KintoneModel
{

const FIELD_PARK_NAME = "駐車場名";
const FIELD_ROOM_NO = "車室番号";
const FIELD_ROOM_NAME = "車室名";
const FIELD_FULL = "満空";
const FIELD_CAN_USE_VEHICLE_TYPE = "利用可能車種";
const FIELD_CAN_DEVIDE_PER_DAY = "日割り";
const FIELD_TABLE_FEE = "料金表";

const FIELD_TABLE_FEE_VEHICLE_TYPE = "車種";
const FIELD_TABLE_FEE_AMOUNT_PER_MONTH = "金額_1ヶ月";


protected array $fields = [
self::FIELD_PARK_NAME => FieldType::STRING,
self::FIELD_ROOM_NO => FieldType::STRING,
self::FIELD_ROOM_NAME => FieldType::STRING,
self::FIELD_FULL => FieldType::STRING,
self::FIELD_CAN_USE_VEHICLE_TYPE => FieldType::ARRAY,
self::FIELD_TABLE_FEE => [
self::FIELD_TABLE_FEE_VEHICLE_TYPE => FieldType::STRING,
self::FIELD_TABLE_FEE_AMOUNT_PER_MONTH => FieldType::STRING,
],
];

protected function setModelData(array $data): bool
{
return true;
}

protected function toArrayModel(): array
{
return [];
}
}

+ 15
- 0
app/Kintone/Models/Customer.php Näytä tiedosto

@@ -0,0 +1,15 @@
<?php

namespace App\Kintone\Models;

class Customer extends KintoneModel
{

const FIELD_CUSTOMER_NAME = "顧客名";
const FIELD_CUSTOMER_MEMO = "備考";

protected array $fields = [
self::FIELD_CUSTOMER_NAME => FieldType::STRING,
self::FIELD_CUSTOMER_MEMO => FieldType::STRING,
];
}

+ 12
- 0
app/Kintone/Models/FieldType.php Näytä tiedosto

@@ -0,0 +1,12 @@
<?php

namespace App\Kintone\Models;

enum FieldType
{
case STRING;
case ARRAY;
case DATETIME;
case USERS;
case TABLE;
}

+ 237
- 0
app/Kintone/Models/KintoneModel.php Näytä tiedosto

@@ -0,0 +1,237 @@
<?php

namespace App\Kintone\Models;

use App\Kintone\KintoneRecordQuery;
use App\Util\DateUtil;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use stdClass;

abstract class KintoneModel
{

const FIELD_RECORD_NUMBER = "レコード番号";
const FIELD_CREATED_TIME = "作成日時";
const FIELD_UPDATED_TIME = "更新日時";

protected ?int $recordId = null;

protected ?int $revision = null;

protected ?Carbon $createdAt = null;

protected ?Carbon $updatedAt = null;

protected ?stdClass $dataOrigin = null;
protected stdClass $data;

protected array $fields = [];

private array $changed = [];

public function __construct()
{
$this->data = new stdClass();
}

public function set(string $fieldCode, $value)
{
$field = Arr::get($this->fields, $fieldCode);
if ($field instanceof FieldType) {
$this->setData($fieldCode, $field, $value);
} else if (is_array($field)) {
$this->setTable($fieldCode, $value);
}

data_set($this->changed, $fieldCode, true);
return $this;
}

private function setTable(string $fieldCode, array $table)
{
foreach ($table as $index => $row) {
$row = data_get($row, "value");
$this->setTableRow($fieldCode, $index, $row);
}
}

private function setTableRow(string $fieldCode, int $index, array $row)
{
foreach ($row as $columnFieldCode => $column) {
$value = $column["value"];
$type = $this->fields[$fieldCode][$columnFieldCode];
$insertKey = sprintf("%s.%d.%s", $fieldCode, $index, $columnFieldCode);
$this->setData($insertKey, $type, $value);
}
}

private function setData(string $path, FieldType $type, $value)
{
if (
$type === FieldType::STRING ||
$type === FieldType::ARRAY
) {
data_set($this->data, $path, $value);
// logger([$path, $value]);
return $this;
}
if ($type === FieldType::DATETIME) {
data_set($this->data, $path, DateUtil::parse($value));
return $this;
}
}

public function setDataFromRecordResponse(array $data): bool
{
$ret = true;
foreach ($data as $fieldCode => $ele) {
$type = data_get($ele, "type");
$value = data_get($ele, "value");
if ($type === "RECORD_NUMBER") {
$this->recordId = $value;
continue;
}
if ($type === "__REVISION__") {
$this->revision = $value;
continue;
}
if ($type === "CREATED_TIME") {
$this->createdAt = DateUtil::parse($value);
continue;
}
if ($type === "UPDATED_TIME") {
$this->updatedAt = DateUtil::parse($value);
continue;
}

$this->set($fieldCode, $value);
}

$ret = $this->setDataCustom($data);
if ($ret) {
$this->clean();
}
return $ret;
}

/**
* 変更前データを現在データで上書きする
*
* @return void
*/
public function clean(?int $revision = null)
{
$this->dataOrigin = clone $this->data;
if ($revision !== null) {
$this->revision = $revision;
}
$this->changed = [];
}

public function getApiLayout(): array
{
$ret = [];
foreach ($this->fields as $fieldCode => $type) {

// 変更があった項目のみレイアウトへ出力する
if (!Arr::has($this->changed, $fieldCode)) {
continue;
}

$path = sprintf("%s.value", $fieldCode);
if ($type === FieldType::STRING) {
data_set($ret, $path, data_get($this->data, $fieldCode));
continue;
}
if ($type === FieldType::ARRAY) {
data_set($ret, $path, data_get($this->data, $fieldCode));
continue;
}
if ($type === FieldType::DATETIME) {
data_set($ret, $path, $this->getDate($fieldCode)->toIso8601ZuluString());
continue;
}
}

return array_merge($ret, $this->getApiLayoutCustom());
}

public function get(string $key)
{
return data_get($this->data, $key);
}

public function getStr(string $key): ?string
{
return $this->get($key);
}

public function getNumber(string $key): ?int
{
return $this->get($key);
}

public function getDate(string $key): ?Carbon
{
return $this->get($key);
}

public function getTable(string $key): ?array
{
return $this->get($key);
}

public function getRecordId(): ?int
{
return $this->recordId;
}
public function setRecordId(int $recordId): void
{
$this->recordId = $recordId;
}

public function getRevision(): ?int
{
return $this->revision;
}

public function setRevision(int $revision): void
{
$this->revision = $revision;
}

public function getUpdatedAt(): ?Carbon
{
return $this->updatedAt;
}
public function getCreatedAt(): ?Carbon
{
return $this->createdAt;
}

static public function query()
{
return new KintoneRecordQuery(static::class);
}

/**
* オーバーライドを期待
*
* @param array $data
* @return boolean
*/
protected function setDataCustom(array $data): bool
{
return true;
}
/**
* オーバーライドを期待
*
* @return array
*/
protected function getApiLayoutCustom(): array
{
return [];
}
}

+ 15
- 0
app/Kintone/Models/SeasonTicketContract.php Näytä tiedosto

@@ -0,0 +1,15 @@
<?php

namespace App\Kintone\Models;

class SeasonTicketContract extends KintoneModel
{

const FIELD_CUSTOMER_NAME = "顧客名";
const FIELD_CUSTOMER_MEMO = "備考";

protected array $fields = [
self::FIELD_CUSTOMER_NAME => FieldType::STRING,
self::FIELD_CUSTOMER_MEMO => FieldType::STRING,
];
}

+ 27
- 0
app/Kintone/Models/Small.php Näytä tiedosto

@@ -0,0 +1,27 @@
<?php

namespace App\Kintone\Models;


class Small extends KintoneModel
{

const FIELD_NAME = "名前";
const FIELD_AGE = "年齢";


protected array $fields = [
self::FIELD_NAME => FieldType::STRING,
self::FIELD_AGE => FieldType::STRING,
];

protected function setModelData(array $data): bool
{
return true;
}

protected function toArrayModel(): array
{
return [];
}
}

+ 37
- 0
app/Listeners/Email/EmailSendJobRegister.php Näytä tiedosto

@@ -0,0 +1,37 @@
<?php

namespace App\Listeners\Email;

use App\Events\Email\ConfirmEvent;
use App\Jobs\Email\SimpleEmail;
use App\Util\DateUtil;

class EmailSendJobRegister
{

/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param ConfirmEvent $event
* @return void
*/
public function handle(ConfirmEvent $event)
{
$email = $event->email;
if ($email->confirm_datetime === null && $email->send_datetime === null) {

$email->confirm_datetime = DateUtil::now();
$email->save();

SimpleEmail::dispatch($event->email);
}
}
}

+ 15
- 0
app/Listeners/Model/CreatedListener.php Näytä tiedosto

@@ -0,0 +1,15 @@
<?php

namespace App\Listeners\Model;

use App\Events\Model\CreatedEvent;

class CreatedListener extends ModelListener
{
protected const ACTION = '作成';

public function handle(CreatedEvent $event): void
{
$this->handleEvent($event->model);
}
}

+ 15
- 0
app/Listeners/Model/DeletedListener.php Näytä tiedosto

@@ -0,0 +1,15 @@
<?php

namespace App\Listeners\Model;

use App\Events\Model\DeletedEvent;

class DeletedListener extends ModelListener
{
protected const ACTION = '削除';

public function handle(DeletedEvent $event): void
{
$this->handleEvent($event->model);
}
}

+ 15
- 0
app/Listeners/Model/DeletingListener.php Näytä tiedosto

@@ -0,0 +1,15 @@
<?php

namespace App\Listeners\Model;

use App\Events\Model\DeletingEvent;

class DeletingListener extends ModelListener
{
protected const ACTION = '削除';

public function handle(DeletingEvent $event): void
{
$this->handleEvent($event->model);
}
}

+ 48
- 0
app/Listeners/Model/ModelListener.php Näytä tiedosto

@@ -0,0 +1,48 @@
<?php

namespace App\Listeners\Model;

use App\Models\ColumnName;
use App\Models\Feature\IModelFeature;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;

abstract class ModelListener
{

protected const ACTION = '-';

protected function handleEvent(IModelFeature $model): void
{
// 履歴作成処理
$this->createHistory($model);
}

protected function createHistory(IModelFeature $model)
{

$history = $model->getHistory();
$changeMessage = "";
if ($history !== null) {
$history->fillFromOrigin($model);
$history->save();

if ($model instanceof Model) {
$before = $model->getOriginal();
$after = $model;
$message = $model->getChangeLogMessage($before, $after);
if ($message !== null) {
$changeMessage = sprintf("[%s]", $message);
}
}
}
Log::info(sprintf(
"モデル変更検知[%s][%s][ID:%s]%s",
$model->getModelName(),
static::ACTION,
data_get($model, ColumnName::ID),
$changeMessage
));
}
}

+ 25
- 0
app/Listeners/Model/UpdatingListener.php Näytä tiedosto

@@ -0,0 +1,25 @@
<?php

namespace App\Listeners\Model;

use App\Events\Model\UpdatingEvent;
use Illuminate\Support\Facades\Auth;

class UpdatingListener extends ModelListener
{
protected const ACTION = '更新';

public function handle(UpdatingEvent $event): void
{
// 更新者作成者の設定
if (Auth::check()) {
$id = Auth::id();
$event->model->updated_by = $id;
if ($event->model->created_by === null) {
$event->model->created_by = $id;
}
}

$this->handleEvent($event->model);
}
}

+ 14
- 0
app/Middlewares/Now.php Näytä tiedosto

@@ -0,0 +1,14 @@
<?php

namespace App\Middlewares;

use Illuminate\Support\Carbon;

class Now
{

static public function get()
{
return new Carbon();
}
}

+ 49
- 0
app/Models/AppModel.php Näytä tiedosto

@@ -0,0 +1,49 @@
<?php

namespace App\Models;

use Exception;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;

abstract class AppModel extends BaseModel
{
use SoftDeletes, HasUuids;

public function getHistory(): ?HistoryModel
{
$historyName = static::class . 'History';


$history = new $historyName;

if ($history instanceof HistoryModel) {
if (Auth::check()) {
$id = Auth::id();
$history->updated_by = $id;
$history->created_by = $id;
}
return $history;
} else {
throw new Exception("履歴モデル不正");
}
}

public function getChangeLogMessage($before, $after): ?string
{
return null;
}

public function setId(?string $uuid = null)
{
if ($this->id !== null) return;

if ($uuid) {
$this->id = $uuid;
} else {
$this->id = Str::uuid();
}
}
}

+ 67
- 0
app/Models/BaseModel.php Näytä tiedosto

@@ -0,0 +1,67 @@
<?php

namespace App\Models;

use Auth;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

/**
* @property int|null $created_by
*/
abstract class BaseModel extends Model
{
use HasFactory;

const COL_NAME_ID = ColumnName::ID;
const COL_NAME_CREATED_BY = 'created_by';
const COL_NAME_CREATED_AT = self::CREATED_AT;
const COL_NAME_UPDATED_AT = self::UPDATED_AT;

public function __construct(array $attr = [])
{
if (Auth::check()) {
$this->created_by = Auth::id();
}

parent::__construct($attr);
}

protected $guarded = [
self::COL_NAME_ID,
];

public static function getBuilder(string $name = 'main')
{
return DB::table(static::getTableName(), $name);
}

public static function getTableName(): string
{
return (new static)->getTable();
}

public function copy(BaseModel|User $from)
{
$data = $from->getAttributeKeys();

foreach ($data as $key) {
if ($key === ColumnName::ID && $this instanceof BaseHistory) {
continue;
}
$this->$key = $from->$key;
}
return $this;
}

public function getAttributeKeys()
{
return array_values(array_unique(array_merge(array_keys($this->attributesToArray()), $this->hidden)));
}

public function isNotSavedModel(): bool
{
return data_get($this, self::COL_NAME_ID) === null;
}
}

+ 38
- 0
app/Models/Feature/IModelFeature.php Näytä tiedosto

@@ -0,0 +1,38 @@
<?php

namespace App\Models\Feature;

use App\Models\HistoryModel;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;

/**
* @property ?Carbon $updated_at
* @property ?Carbon $created_at
* @property ?string $updated_by
* @property ?string $created_by
*/
interface IModelFeature
{

public static function getBuilder(string $name = 'main'): Builder;

public static function getTableName(): string;

public function copy(IModelFeature $from): static;

public function getAttributeKeys(): array;

public function isNotSavedModel(): bool;

public function getHistory(): HistoryModel|null;

/**
* モデルの和名を取得する
*
* @return string
*/
public function getModelName(): string;

public function getChangeLogMessage($before, $after): string|null;
}

+ 39
- 0
app/Models/HistoryModel.php Näytä tiedosto

@@ -0,0 +1,39 @@
<?php

namespace App\Models;

use App\Models\Feature\IModelFeature;
use Illuminate\Database\Eloquent\Collection;

abstract class HistoryModel extends BaseModel
{
const COL_NAME_HISTORY_ID = ColumnName::HISTORY_ID;

protected $primaryKey = ColumnName::HISTORY_ID;

/**
* @param string $id
* @return Collection<static>
*/
public static function findById(string $id)
{
return static::query()->where(ColumnName::ID, $id)
->orderBy(ColumnName::CREATED_AT)
->get();
}

public function fillFromOrigin(IModelFeature $originModel)
{
return $this->copy($originModel);
}

public function getHistory(): ?HistoryModel
{
return null;
}

public function getChangeLogMessage($before, $after): ?string
{
return null;
}
}

+ 109
- 0
app/Models/User.php Näytä tiedosto

@@ -0,0 +1,109 @@
<?php

namespace App\Models;

use App\Codes\UserRole;
use App\Events\Model\CreatedEvent;
use App\Events\Model\DeletedEvent;
use App\Events\Model\UpdatingEvent;
use App\Models\Feature\IModelFeature;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\DB;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements IModelFeature
{
use HasApiTokens, HasFactory, Notifiable, HasUuids, SoftDeletes, ContractFeature;

const COL_NAME_ID = 'id';
const COL_NAME_ROLE = 'role';
const COL_NAME_EMAIL = 'email';
const COL_NAME_NAME = 'name';
const COL_NAME_PASSWORD = 'password';

const COL_NAME_CONTRACT_ID = ColumnName::CONTRACT_ID;

const COL_NAME_CREATED_BY = ColumnName::CREATED_BY;
const COL_NAME_UPDATED_BY = ColumnName::UPDATED_BY;
const COL_NAME_CREATED_AT = ColumnName::CREATED_AT;
const COL_NAME_UPDATED_AT = ColumnName::UPDATED_AT;
const COL_NAME_DELETED_AT = ColumnName::DELETED_AT;

/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
self::COL_NAME_PASSWORD,
];

protected $guarded = [
self::COL_NAME_ID,
self::COL_NAME_CREATED_BY,
self::COL_NAME_UPDATED_BY,
self::COL_NAME_CREATED_AT,
self::COL_NAME_UPDATED_AT,
self::COL_NAME_DELETED_AT,
];

protected $casts = [
self::COL_NAME_ROLE => UserRole::class,
];

protected $dispatchesEvents = [
'created' => CreatedEvent::class,
'updating' => UpdatingEvent::class,
'deleted' => DeletedEvent::class,
];

public static function getBuilder(string $name = 'main'): Builder
{
return DB::table(static::getTableName(), $name);
}

public static function getTableName(): string
{
return (new static)->getTable();
}

public function copy(IModelFeature $from): static
{
$data = $from->getAttributeKeys();

foreach ($data as $key) {
$this->$key = $from->$key;
}
return $this;
}

public function getAttributeKeys(): array
{
return array_values(array_unique(array_merge(array_keys($this->attributesToArray()), $this->hidden)));
}

public function isNotSavedModel(): bool
{
return data_get($this, ColumnName::ID) === null;
}

public function getHistory(): ?HistoryModel
{
return new UserHistory();
}

public function getModelName(): string
{
return "ユーザー情報";
}

public function getChangeLogMessage($before, $after): ?string
{
return null;
}
}

+ 12
- 0
app/Models/UserHistory.php Näytä tiedosto

@@ -0,0 +1,12 @@
<?php

namespace App\Models;


class UserHistory extends HistoryModel
{
public function getModelName(): string
{
return "ユーザー情報履歴";
}
}

+ 48
- 0
app/Providers/AppServiceProvider.php Näytä tiedosto

@@ -0,0 +1,48 @@
<?php

namespace App\Providers;

use App\Codes\EnvironmentName;
use App\Codes\QueueName;
use App\Codes\SMSProviderName;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}

/**
* Bootstrap any application services.
*/
public function boot(): void
{
if ($this->app->environment(EnvironmentName::LOCAL->value)) {
// IDEヘルパー登録
$this->app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
}

//Queue関連
Queue::before(function (JobProcessing $event) {
// Logのドライバー設定
$queueName = $event->job->getQueue();
if ($queueName === QueueName::EMAIL->value) {
Log::setDefaultDriver('queue-email');
} else if ($queueName === QueueName::JOB->value) {
Log::setDefaultDriver('queue-job');
}
});


// DB関連
$this->app->singleton(\App\Util\DBUtil::class);
}
}

+ 26
- 0
app/Providers/AuthServiceProvider.php Näytä tiedosto

@@ -0,0 +1,26 @@
<?php

namespace App\Providers;

// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];

/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
//
}
}

+ 19
- 0
app/Providers/BroadcastServiceProvider.php Näytä tiedosto

@@ -0,0 +1,19 @@
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;

class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Broadcast::routes();

require base_path('routes/channels.php');
}
}

+ 38
- 0
app/Providers/EventServiceProvider.php Näytä tiedosto

@@ -0,0 +1,38 @@
<?php

namespace App\Providers;

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];

/**
* Register any events for your application.
*/
public function boot(): void
{
//
}

/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

+ 48
- 0
app/Providers/RouteServiceProvider.php Näytä tiedosto

@@ -0,0 +1,48 @@
<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/home';

/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
$this->configureRateLimiting();

$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));

Route::middleware('web')
->group(base_path('routes/web.php'));
});
}

/**
* Configure the rate limiters for the application.
*/
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
}

+ 67
- 0
app/Repositories/BaseRepository.php Näytä tiedosto

@@ -0,0 +1,67 @@
<?php

namespace App\Repositories;

use Illuminate\Database\Query\Builder;

abstract class BaseRepository
{

const ORDER_ASC = "asc";
const ORDER_DESC = "desc";

const CONDITION_SORT_TARGET = "sort";
const CONDITION_SORT_ORDER = "order";

const CONDITION_LIMIT = "limit";
const MAX_LIMIT = 500;


protected static function sort(Builder $query, array $condition)
{
$target = data_get($condition, self::CONDITION_SORT_TARGET);
$order = data_get($condition, self::CONDITION_SORT_ORDER);
if ($target === null || $order === null) {
return;
}

if ($order === self::ORDER_ASC) {
$query->orderBy($target);
}
if ($order === self::ORDER_DESC) {
$query->orderByDesc($target);
}
}

protected static function limit(Builder $query, array $condition, int $default = self::MAX_LIMIT)
{
$limit = data_get($condition, self::CONDITION_LIMIT, $default) ?? $default;

$limit = min($limit, self::MAX_LIMIT);

$query->limit($limit);
}

protected static function where(Builder $query, array $condition, string $conditionKey, string|null $columnName = null): bool
{

$ret = data_get($condition, $conditionKey);
if ($ret !== null) {
$query->where($columnName ?? $conditionKey, $ret);
return true;
} else {
return false;
}
}
protected static function whereIn(Builder $query, array $condition, string $conditionKey, string|null $columnName = null): bool
{

$ret = data_get($condition, $conditionKey);
if ($ret !== null && is_array($ret)) {
$query->whereIn($columnName ?? $conditionKey, $ret);
return true;
} else {
return false;
}
}
}

+ 29
- 0
app/Repositories/BaseRepositoryData.php Näytä tiedosto

@@ -0,0 +1,29 @@
<?php

namespace App\Repositories;

use Illuminate\Support\Collection;
use stdClass;

class BaseRepositoryData extends stdClass
{
public function __construct(stdClass $data)
{
foreach ($data as $key => $val) {
$this->$key = $val;
}
}

/**
* @param Collection<stdClass> $list
* @return Collection<static>
*/
static public function makeList(Collection $list)
{
$ret = collect();
foreach ($list as $data) {
$ret->push(new static($data));
}
return $ret;
}
}

+ 32
- 0
app/Rules/BaseRule.php Näytä tiedosto

@@ -0,0 +1,32 @@
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

abstract class BaseRule implements Rule
{
abstract public function check($value) : bool;

/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return $this->check($value);
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return '正しくありません。';
}
}

+ 34
- 0
app/Rules/Kana.php Näytä tiedosto

@@ -0,0 +1,34 @@
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class Kana extends BaseRule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}


public function check($value): bool
{
return preg_match("/^[ァ-ヾ  ]+$/u", $value) !== 0;
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'カナ文字を入力してください。';
}
}

+ 60
- 0
app/Rules/LoginPassword.php Näytä tiedosto

@@ -0,0 +1,60 @@
<?php

namespace App\Rules;

use App\Models\Htpms\MstCustomer;

class LoginPassword extends BaseRule
{

private string $message = '';

private int $min = 8;
private int $max = 20;

/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
}

public function check($value): bool
{
if (!$this->length($value)) return false;
if (!$this->charactor($value)) return false;
return true;
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return $this->message;
}

private function length(string $value)
{
$len = strlen($value);
if (8 <= $len && $len <= 20) {
return true;
} else {
$this->message = "{$this->min}文字以上 {$this->max}以下で設定してください";
return false;
}
}

private function charactor(string $value)
{
if (preg_match("/^[a-zA-Z0-9@.\-]+$/", $value) === 0) {
$this->message = "半角英数字と記号(@.-)のみ設定可能です。";
return false;
}
return true;
}
}

+ 33
- 0
app/Rules/NotIncludeCode.php Näytä tiedosto

@@ -0,0 +1,33 @@
<?php

namespace App\Rules;


class NotIncludeCode extends BaseRule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}


public function check($value): bool
{
return preg_match("/[!-\/:-@\[-`{-~]*/", $value) !== 0;
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return '正しくありません';
}
}

+ 32
- 0
app/Rules/PhoneNumber.php Näytä tiedosto

@@ -0,0 +1,32 @@
<?php

namespace App\Rules;

class PhoneNumber extends BaseRule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}


public function check($value) : bool
{
return preg_match("/^\d{10,11}$/", $value) !== 0;
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return '正しくありません';
}
}

+ 32
- 0
app/Rules/ZipCode.php Näytä tiedosto

@@ -0,0 +1,32 @@
<?php

namespace App\Rules;

class ZipCode extends BaseRule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}


public function check($value) : bool
{
return preg_match("/^\d{7}$/", $value) !== 0;
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return '正しい郵便番号を入力してください';
}
}

+ 56
- 0
app/Util/DBUtil.php Näytä tiedosto

@@ -0,0 +1,56 @@
<?php

namespace App\Util;

use App\Features\InstanceAble;
use Illuminate\Support\Facades\DB;
use LogicException;

class DBUtil
{
use InstanceAble;

private bool $isBeginning = false;

public function __destruct()
{
if ($this->isBeginning) {
$this->rollBack();
}
}

public function beginTransaction(): void
{
if ($this->isBeginning) {
throw new LogicException("2重トランザクション開始検知");
}

DB::beginTransaction();
$this->isBeginning = true;
}

public function commit(): void
{
if (!$this->isBeginning) {
throw new LogicException("無効なコミット検知");
}
DB::commit();
$this->isBeginning = false;
}

public function rollBack(): void
{
if (!$this->isBeginning) {
throw new LogicException("無効なロールバック検知");
}

DB::rollBack();
logs()->warning("ロールバック検知");
$this->isBeginning = false;
}

public function isBeginning(): bool
{
return $this->isBeginning;
}
}

+ 41
- 0
app/Util/DateUtil.php Näytä tiedosto

@@ -0,0 +1,41 @@
<?php

namespace App\Util;

use Illuminate\Support\Carbon;

class DateUtil
{
public static function now()
{
if (!app()->environment('local')) {
return new Carbon();
}

$nowStr = self::getConfig();

if ($nowStr !== null && $nowStr !== '') {
$date = new Carbon($nowStr);
if ($date->isValid()) {
return new Carbon();
return $date;
}
}
return new Carbon();
}

public static function parse(string $source): Carbon|null
{
$date = Carbon::parse($source);
if ($date->isValid()) {
return $date->timezone(config('app.timezone'));
}
return null;
}


private static function getConfig()
{
return config('date.now', null);
}
}

+ 22
- 0
app/Util/KeysConverter.php Näytä tiedosto

@@ -0,0 +1,22 @@
<?php

namespace App\Util;

use App\Models\Htpms\MstCustomer;
use App\Models\HtpmsCustomer\TblPark;

class KeysConverter
{
public static function getTblPark(string $customerCode, string $parkingManagementCode) :TblPark
{
TblPark::setCustomerId(self::getCustomerId($customerCode));
$park = TblPark::parkingManagementCode($parkingManagementCode)->firstOrFail();
return $park;
}

public static function getCustomerId(string $customerCode)
{
$customer = MstCustomer::customerCode($customerCode)->firstOrFail();
return $customer->id;
}
}

+ 171
- 0
app/Util/MigrationHelper.php Näytä tiedosto

@@ -0,0 +1,171 @@
<?php

namespace App\Util;

use App\Models\ColumnName;
use App\Models\Contract;
use App\Models\Feature\IModelFeature;
use App\Models\ReceiptIssuingOrder;
use App\Models\SMSProvider;
use App\Models\SMSSendOrder;
use App\Models\User;
use Closure;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

class MigrationHelper
{

public static function createTable(string $tableName, Closure $schema)
{
Schema::create($tableName, function (Blueprint $table) use ($tableName, $schema) {

$modelClassName = Str::singular('App\\Models\\' . Str::studly($tableName));
if (class_exists($modelClassName)) {

/**
* @var IModelFeature
*/
$model = new $modelClassName();
$table->comment($model->getModelName());
}

$forHistory = false;
if (Str::endsWith($tableName, "histories")) {
$forHistory = true;
}
$helper = new MigrationHelper($table, $forHistory);
$schema($table, $helper);
});
}

public static function alterTable(string $tableName, Closure $schema)
{
Schema::table($tableName, function (Blueprint $table) use ($tableName, $schema) {
$forHistory = false;
if (Str::endsWith($tableName, "histories")) {
$forHistory = true;
}
$helper = new MigrationHelper($table, $forHistory);
$schema($table, $helper);
});
}



private Blueprint $table;
private bool $forHistory = false;

public function __construct(Blueprint $table, bool $forHistory = false)
{
$this->table = $table;
$this->forHistory = $forHistory;
}


public function baseColumn()
{
if ($this->forHistory) {
$this->table->id('history_id')->comment("履歴ID");
$this->table->uuid('id')->comment("ID");
} else {
$this->table->uuid('id')->primary()->comment("ID");
}
$this->table->uuid(ColumnName::CREATED_BY)->nullable()->comment("作成者ID");
$this->table->uuid(ColumnName::UPDATED_BY)->nullable()->comment("更新者ID");
$this->table->timestamp(ColumnName::CREATED_AT)->nullable()->comment("作成日時");
$this->table->timestamp(ColumnName::UPDATED_AT)->nullable()->comment("更新日時");
$this->table->timestamp(ColumnName::DELETED_AT)->nullable()->comment("論理削除日時");

$this->table->index([ColumnName::CREATED_AT], sprintf("%s_idx_CREATED_AT", $this->table->getTable()));
$this->table->index([ColumnName::UPDATED_AT], sprintf("%s_idx_UPDATED_AT", $this->table->getTable()));

return $this;
}

public function index(int $number, array $columns)
{
$indexName = $this->getIndexName($number);
if ($this->forHistory) {
$this->table->index($columns, $indexName);
} else {
$this->table->index([...$columns, ColumnName::DELETED_AT], $indexName);
}
return $this;
}

public function unique(int $number, array $columns)
{
$uniqueName = $this->getUniqueName($number);
if ($this->forHistory) {
$this->table->unique($columns, $uniqueName);
} else {
$this->table->unique([...$columns, ColumnName::DELETED_AT,], $uniqueName);
}
return $this;
}

public function dropIndex(int $number)
{
$indexName = $this->getIndexName($number);
$this->table->dropIndex($indexName);
return $this;
}

private function getIndexName(int $number)
{
return sprintf("%s_idx_%02d", $this->table->getTable(), $number);
}

private function getUniqueName(int $number)
{
return sprintf("%s_uq_%02d", $this->table->getTable(), $number);
}


public function contractId(bool $nullable = false)
{
$this->table->uuid(ColumnName::CONTRACT_ID)->comment("契約ID")->nullable($nullable);
// $this->table->foreign(ColumnName::CONTRACT_ID)->references(ColumnName::ID)->on(Contract::getTableName());

return $this;
}

public function receiptIssuingOrderId(bool $nullable = false)
{
$this->table->uuid(ColumnName::RECEIPT_ISSUING_ORDER_ID)->comment("領収証発行依頼ID")->nullable($nullable);
// $this->table->foreign(ColumnName::RECEIPT_ISSUING_ORDER_ID)->references(ColumnName::ID)->on(ReceiptIssuingOrder::getTableName());
return $this;
}

public function smsSendOrderId(bool $nullable = false)
{
$this->table->uuid(ColumnName::SMS_SEND_ORDER_ID)->comment("SMS送信依頼ID")->nullable($nullable);
// $this->table->foreign(ColumnName::SMS_SEND_ORDER_ID)->references(ColumnName::ID)->on(SMSSendOrder::getTableName());
return $this;
}

public function userId(bool $nullable = false, ?string $columnName = null, ?string $comment = null)
{

$columnName = $columnName ?? ColumnName::USER_ID;
$comment = $comment ?? "ユーザーID";
$this->table->uuid($columnName)->comment($comment)->nullable($nullable);
// $this->table->foreign($columnName)->references(ColumnName::ID)->on(User::getTableName());
return $this;
}

public function smsProviderId(bool $nullable = false)
{
$this->table->uuid(ColumnName::SMS_PROVIDER_ID)->comment("SMSプロバイダーID")->nullable($nullable);
// $this->table->foreign(ColumnName::SMS_PROVIDER_ID)->references(ColumnName::ID)->on(SMSProvider::getTableName());
return $this;
}

public function emailId(bool $nullable = false)
{
$this->table->uuid(ColumnName::EMAIL_ID)->comment("EメールID")->nullable($nullable);
return $this;
}
}

+ 18
- 0
app/Util/OptionUtil.php Näytä tiedosto

@@ -0,0 +1,18 @@
<?php

namespace App\Util;

use Illuminate\Support\Collection;

class OptionUtil
{

static public function getOptions(Collection|array $data)
{
$ret = collect();
foreach ($data as $key => $val) {
$ret->push([$key => $val]);
}
return $ret;
}
}

+ 41
- 0
app/Util/RouteHelper.php Näytä tiedosto

@@ -0,0 +1,41 @@
<?php

namespace App\Util;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;

class RouteHelper
{

const ENTRY = 'entry';

static public function get(string $url, string $class)
{
return Route::get($url, [$class, self::ENTRY])->name(self::routeName($class));
}

static public function post(string $url, string $class)
{
return Route::post($url, [$class, self::ENTRY])->name(self::routeName($class));
}

static public function server(string $url, string $class)
{
return Route::post($url, [$class, self::ENTRY])->name(self::routeName($class));
}

static public function routeName(string $class)
{
$ele = explode('\\', $class);
$controllerName = array_pop($ele);
$groupName = array_pop($ele);
$routeName = Str::replaceLast('Controller', '', $groupName . $controllerName);
return $routeName;
}

static public function webRoute(string $route)
{
return Str::replaceFirst('/api', '', $route);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Peruuta
Tallenna