Merge branch 'main' into streamline-installer
This commit is contained in:
commit
e6b052d109
@ -28,7 +28,5 @@ indent_size = 4
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json, .travis.yml}]
|
||||
indent_style = space
|
||||
[justfile]
|
||||
indent_size = 2
|
||||
|
||||
263
.github/workflows/build.yml
vendored
263
.github/workflows/build.yml
vendored
@ -15,39 +15,24 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# ── Static analysis (combined — one checkout/setup, separate step results) ─
|
||||
static-analysis:
|
||||
name: "Static Analysis"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php-versions: ["8.3", "8.4", "8.5"]
|
||||
name: PHP ${{ matrix.php-versions }}
|
||||
env:
|
||||
extensions: intl, pcov, mbstring
|
||||
key: cache-v1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: Act Workaround # https://github.com/nektos/act/issues/973
|
||||
if: ${{ env.ACT }}
|
||||
run: curl -fsSL https://deb.nodesource.com/setup_22.x | bash && apt install -y nodejs
|
||||
|
||||
# Configure Caching
|
||||
- name: Setup cache environment
|
||||
id: cache-env
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: ${{ env.extensions }}
|
||||
key: ${{ env.key }}
|
||||
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.cache-env.outputs.dir }}
|
||||
key: ${{ steps.cache-env.outputs.key }}
|
||||
restore-keys: ${{ steps.cache-env.outputs.key }}
|
||||
php-version: "8.3"
|
||||
extensions: intl, mbstring
|
||||
coverage: none
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
@ -59,39 +44,22 @@ jobs:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
||||
# Configure PHP
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: ${{ env.extensions }}
|
||||
ini-values: post_max_size=256M, short_open_tag=On
|
||||
coverage: xdebug
|
||||
tools: php-cs-fixer, phpunit
|
||||
|
||||
# Bootstrap order has to thread a needle:
|
||||
# 1. composer install --no-scripts: pulls vendor/ (needed by Vite
|
||||
# because theme.css @imports vendor/filament/filament/resources/css)
|
||||
# WITHOUT firing post-autoload-dump (which boots service providers
|
||||
# that read the Vite manifest + dist files we haven't built yet).
|
||||
# 2. npm install + npm run build: produces public/build/manifest.json
|
||||
# and (re)writes resources/js/dist/admin/components/*.js.
|
||||
# 3. composer dump-autoload: re-fires post-autoload-dump now that the
|
||||
# frontend artifacts exist; package:discover and filament:upgrade
|
||||
# boot providers cleanly.
|
||||
- name: Install Composer dependencies (no scripts)
|
||||
run: |
|
||||
php --version
|
||||
composer install --dev --no-interaction --no-scripts --verbose
|
||||
composer global require laravel/pint
|
||||
run: composer install --dev --no-interaction --no-scripts --verbose
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install NPM dependencies
|
||||
run: |
|
||||
bun install
|
||||
bun run fmt config/version.yml
|
||||
run: bun install
|
||||
|
||||
- name: Lint & Format Check
|
||||
run: bun run lint && bun run fmt:check
|
||||
- name: Run Pint
|
||||
run: vendor/bin/pint --test --parallel
|
||||
|
||||
- name: Lint Check (oxlint)
|
||||
run: bun run lint
|
||||
|
||||
- name: Format Check (oxfmt)
|
||||
run: bun run fmt:check
|
||||
|
||||
- name: Compile assets
|
||||
run: bun run build
|
||||
@ -100,22 +68,163 @@ jobs:
|
||||
run: |
|
||||
composer dump-autoload -o
|
||||
cp .github/scripts/env.test .env
|
||||
cp .github/scripts/phpunit.xml phpunit.xml
|
||||
.github/scripts/version.sh
|
||||
|
||||
- name: Run Pint
|
||||
run: pint --test --parallel
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export PHP_CS_FIXER_IGNORE_ENV=1
|
||||
vendor/bin/pest --parallel --ci
|
||||
|
||||
- name: Run PHPStan
|
||||
run: vendor/bin/phpstan analyse --memory-limit=2G
|
||||
|
||||
# This runs after all of the tests, run have run. Creates a cleaned up version of the
|
||||
# distro, and then creates the artifact to push up to S3 or wherever
|
||||
- name: Run Rector (dry-run)
|
||||
if: false
|
||||
run: vendor/bin/rector process --dry-run
|
||||
|
||||
# ── Test matrix: PHP 8.5 × {sqlite, mysql, postgres} ────────────────────
|
||||
test:
|
||||
name: "Tests — ${{ matrix.db }}"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [static-analysis]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
db:
|
||||
# - sqlite
|
||||
- mysql
|
||||
- postgres
|
||||
|
||||
# Both service containers start for every matrix shard. The sqlite shard
|
||||
# ignores them — ~5s overhead per run is negligible compared to the
|
||||
# alternative of maintaining three separate job definitions.
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: phpvms
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h 127.0.0.1 -u root -proot"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
|
||||
postgres:
|
||||
image: postgres:17
|
||||
env:
|
||||
POSTGRES_DB: phpvms
|
||||
POSTGRES_USER: phpvms
|
||||
POSTGRES_PASSWORD: phpvms
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U phpvms"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
|
||||
env:
|
||||
extensions: intl, pcov, mbstring
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Act Workaround # https://github.com/nektos/act/issues/973
|
||||
if: ${{ env.ACT }}
|
||||
run: curl -fsSL https://deb.nodesource.com/setup_22.x | bash && apt install -y nodejs
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.5"
|
||||
extensions: ${{ matrix.db == 'mysql' && 'intl, pcov, mbstring, pdo_mysql, mysqli' || matrix.db == 'postgres' && 'intl, pcov, mbstring, pdo_pgsql, pgsql' || 'intl, pcov, mbstring' }}
|
||||
ini-values: post_max_size=256M, short_open_tag=On
|
||||
coverage: xdebug
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
||||
# Bootstrap order (see comment in original build job for rationale):
|
||||
# 1. composer install --no-scripts
|
||||
# 2. bun install + build (produces Vite manifest + dist)
|
||||
# 3. composer dump-autoload + .env + version
|
||||
- name: Install Composer dependencies (no scripts)
|
||||
run: |
|
||||
php --version
|
||||
composer install --dev --no-interaction --no-scripts --verbose
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install NPM dependencies
|
||||
run: |
|
||||
bun install
|
||||
bun run fmt config/version.yml
|
||||
|
||||
- name: Compile assets
|
||||
run: bun run build
|
||||
|
||||
- name: Run Composer post-autoload scripts
|
||||
run: |
|
||||
composer dump-autoload -o
|
||||
.github/scripts/version.sh
|
||||
|
||||
# ── Database-specific setup ─────────────────────────────────────────
|
||||
- name: Configure environment (sqlite)
|
||||
if: matrix.db == 'sqlite'
|
||||
run: |
|
||||
cp .github/scripts/env.test .env
|
||||
cp .github/scripts/phpunit.xml phpunit.xml
|
||||
|
||||
- name: Configure environment (mysql)
|
||||
if: matrix.db == 'mysql'
|
||||
run: |
|
||||
cp .github/scripts/env.test .env
|
||||
cp .github/scripts/phpunit.xml phpunit.xml
|
||||
# Override the phpunit.xml DB vars for MySQL service container
|
||||
sed -i 's|name="DB_CONNECTION" value="sqlite"|name="DB_CONNECTION" value="mysql"|' phpunit.xml
|
||||
sed -i 's|name="DB_DATABASE" value=":memory:"|name="DB_DATABASE" value="phpvms"|' phpunit.xml
|
||||
sed -i 's|name="DB_URL" value=""|name="DB_URL" value="mysql://root:root@127.0.0.1:3306/phpvms"|' phpunit.xml
|
||||
# Also update .env so artisan commands see the right DB
|
||||
sed -i 's|DB_CONNECTION="sqlite"|DB_CONNECTION="mysql"|' .env
|
||||
echo 'DB_HOST="127.0.0.1"' >> .env
|
||||
echo 'DB_PORT="3306"' >> .env
|
||||
echo 'DB_DATABASE="phpvms"' >> .env
|
||||
echo 'DB_USERNAME="root"' >> .env
|
||||
echo 'DB_PASSWORD="root"' >> .env
|
||||
|
||||
- name: Configure environment (postgres)
|
||||
if: matrix.db == 'postgres'
|
||||
run: |
|
||||
cp .github/scripts/env.test .env
|
||||
cp .github/scripts/phpunit.xml phpunit.xml
|
||||
# Override phpunit.xml DB vars for PostgreSQL service container
|
||||
sed -i 's|name="DB_CONNECTION" value="sqlite"|name="DB_CONNECTION" value="pgsql"|' phpunit.xml
|
||||
sed -i 's|name="DB_DATABASE" value=":memory:"|name="DB_DATABASE" value="phpvms"|' phpunit.xml
|
||||
sed -i 's|name="DB_URL" value=""|name="DB_URL" value="pgsql://phpvms:phpvms@127.0.0.1:5432/phpvms"|' phpunit.xml
|
||||
# Also update .env so artisan commands see the right DB
|
||||
sed -i 's|DB_CONNECTION="sqlite"|DB_CONNECTION="pgsql"|' .env
|
||||
echo 'DB_HOST="127.0.0.1"' >> .env
|
||||
echo 'DB_PORT="5432"' >> .env
|
||||
echo 'DB_DATABASE="phpvms"' >> .env
|
||||
echo 'DB_USERNAME="phpvms"' >> .env
|
||||
echo 'DB_PASSWORD="phpvms"' >> .env
|
||||
|
||||
- name: Run Pest
|
||||
env:
|
||||
PHP_CS_FIXER_IGNORE_ENV: "1"
|
||||
run: |
|
||||
vendor/bin/pest --ci --parallel --bail
|
||||
#vendor/bin/pest --ci --testsuite=Arch
|
||||
|
||||
# ── Release artifacts (only on main/dev) ──────────────────────────────────
|
||||
artifacts:
|
||||
name: "Create release package"
|
||||
permissions:
|
||||
@ -123,7 +232,7 @@ jobs:
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
needs: [build]
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'phpvms/phpvms' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')
|
||||
steps:
|
||||
@ -132,17 +241,20 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Act Workaround # https://github.com/nektos/act/issues/973
|
||||
if: ${{ env.ACT }}
|
||||
run: curl -fsSL https://deb.nodesource.com/setup_22.x | bash && apt install -y nodejs
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.3"
|
||||
php-version: "8.5"
|
||||
|
||||
- uses: olegtarasov/get-tag@v2.1.2
|
||||
id: tagName
|
||||
|
||||
# Configure Caching
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
@ -154,10 +266,6 @@ jobs:
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
# Three-step bootstrap (see `build` job comment for full rationale):
|
||||
# 1. composer install --no-scripts: vendor/ for Vite CSS @imports
|
||||
# 2. npm build: writes public/build manifest + dist files
|
||||
# 3. composer dump-autoload: re-fires post-autoload-dump cleanly
|
||||
- name: "Install Release Dependencies (no scripts)"
|
||||
run: |
|
||||
rm -rf vendor
|
||||
@ -212,7 +320,7 @@ jobs:
|
||||
attestations: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
needs: [test]
|
||||
if: github.repository == 'phpvms/phpvms' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main')
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -233,10 +341,3 @@ jobs:
|
||||
push: false
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
# - name: Generate artifact attestation
|
||||
# uses: actions/attest-build-provenance@v2
|
||||
# with:
|
||||
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
||||
# push-to-registry: true
|
||||
|
||||
36
.github/workflows/lint.yml
vendored
36
.github/workflows/lint.yml
vendored
@ -1,36 +0,0 @@
|
||||
#---
|
||||
#
|
||||
# Run Laravel Pint
|
||||
# https://laravel.com/docs/11.x/pint#running-tests-on-github-actions
|
||||
#
|
||||
# name: Check Code Style
|
||||
#
|
||||
# on:
|
||||
# pull_request:
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# - dev
|
||||
# - "feature/**"
|
||||
# - "release/**"
|
||||
# - "hotfix/**"
|
||||
#
|
||||
# jobs:
|
||||
# lint:
|
||||
# runs-on: ubuntu-latest
|
||||
# strategy:
|
||||
# fail-fast: true
|
||||
# matrix:
|
||||
# php: [8.4]
|
||||
#
|
||||
# steps:
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v4
|
||||
#
|
||||
# - name: Setup PHP
|
||||
# uses: shivammathur/setup-php@v2
|
||||
# with:
|
||||
# php-version: ${{ matrix.php }}
|
||||
# extensions: json, dom, curl, libxml, mbstring
|
||||
# coverage: none
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -30,6 +30,7 @@ storage/*.sqlite
|
||||
.envrc
|
||||
.env
|
||||
.github/scripts/env.test
|
||||
.mise.local.toml
|
||||
.vagrant
|
||||
.docker
|
||||
#Homestead.yaml
|
||||
@ -108,3 +109,4 @@ modules_statuses.json
|
||||
/phpvms
|
||||
.env.testing
|
||||
/intellij_style.xml
|
||||
/justfile
|
||||
|
||||
105
.mise.toml
Normal file
105
.mise.toml
Normal file
@ -0,0 +1,105 @@
|
||||
|
||||
[env]
|
||||
BUILDKIT_HOST = "docker-container://buildkit"
|
||||
PHPRC = "resources/mise"
|
||||
_.php = {
|
||||
prebuilt_static = true,
|
||||
prebuilt_static_flavor = "bulk",
|
||||
extra_configure_options = "--with-pdo-mysql --with-pdo-pgsql",
|
||||
pie_extensions = "redis/phpredis xdebug/xdebug"
|
||||
}
|
||||
|
||||
[tools]
|
||||
php = "8.5.6"
|
||||
|
||||
[plugins]
|
||||
php = "https://github.com/verzly/mise-php#latest"
|
||||
|
||||
[hooks]
|
||||
postinstall = "composer install"
|
||||
|
||||
[settings]
|
||||
task_output = "interleave"
|
||||
|
||||
# -----------------------------------------------
|
||||
# Local tasks
|
||||
# mise run setup
|
||||
# -----------------------------------------------
|
||||
|
||||
[tasks.setup]
|
||||
run = [
|
||||
"mise install",
|
||||
"mise run run-buildkit-container || true"
|
||||
]
|
||||
|
||||
[tasks.run-buildkit-container]
|
||||
run = "docker run --rm --privileged -d --name buildkit -e BUILDKIT_DEBUG=1 moby/buildkit:latest"
|
||||
|
||||
[tasks.railpack]
|
||||
run = "railpack build ."
|
||||
|
||||
# -----------------------------------------------
|
||||
# Test against real databases
|
||||
# -----------------------------------------------
|
||||
|
||||
[tasks.test-db-up]
|
||||
description = "Start MySQL and PostgreSQL for testing"
|
||||
usage='''
|
||||
arg "[db]" default="mysql" help="Run on mysql or pgsql?" {
|
||||
choices "mysql" "pgsql"
|
||||
}
|
||||
'''
|
||||
run = '''
|
||||
docker compose -f compose.test.yml up {{ usage.db }} -d --wait
|
||||
'''
|
||||
|
||||
[tasks.test-db-down]
|
||||
description = "Stop test databases and remove volumes"
|
||||
usage='''
|
||||
arg "[db]" default="mysql" help="Run on mysql or pgsql?" {
|
||||
choices "mysql" "pgsql"
|
||||
}
|
||||
'''
|
||||
run = '''
|
||||
docker compose -f compose.test.yml down {{ usage.db }} -v
|
||||
'''
|
||||
|
||||
[tasks.'tests:mysql']
|
||||
description = "Run tests against MySQL (port 13306)"
|
||||
depends = [{ task = "test-db-up", args = ["mysql"]}]
|
||||
depends_post = [{ task = "test-db-down", args = ["mysql"]}]
|
||||
env = {
|
||||
DB_CONNECTION = 'mysql',
|
||||
DB_HOST = '127.0.0.1',
|
||||
DB_PORT = "13306",
|
||||
DB_DATABASE = "testing",
|
||||
DB_USERNAME = "root",
|
||||
DB_PASSWORD = "password"
|
||||
}
|
||||
run = [
|
||||
"vendor/bin/pest --compact --parallel --bail"
|
||||
]
|
||||
|
||||
[tasks.'tests:pgsql']
|
||||
description = "Run tests against PostgreSQL (port 15432)"
|
||||
depends = [{ task = "test-db-up", args = ["pgsql"]}]
|
||||
depends_post = [{ task = "test-db-down", args = ["pgsql"]}]
|
||||
env = {
|
||||
DB_CONNECTION = 'pgsql',
|
||||
DB_HOST = '127.0.0.1',
|
||||
DB_PORT = "15432",
|
||||
DB_DATABASE = "testing",
|
||||
DB_USERNAME = "phpvms",
|
||||
DB_PASSWORD = "password"
|
||||
}
|
||||
run = [
|
||||
"vendor/bin/pest --compact --parallel --bail"
|
||||
]
|
||||
|
||||
# Run the github workflows locally using act
|
||||
[tasks.'tests:actions']
|
||||
run = '''
|
||||
act \
|
||||
--rm \
|
||||
--container-architecture linux/amd64 pull_request
|
||||
'''
|
||||
@ -5,6 +5,7 @@
|
||||
"modules/**",
|
||||
"resources/js/dist/**",
|
||||
".github/skills/*",
|
||||
".github/copilot-instructions.md"
|
||||
".github/copilot-instructions.md",
|
||||
"*.toml"
|
||||
]
|
||||
}
|
||||
|
||||
86
README.md
86
README.md
@ -1,16 +1,12 @@
|
||||
# phpVMS <sup>8</sup>
|
||||
# phpVMS 8 - Build, Fly, Extend
|
||||
|
||||
[](https://github.com/phpvms/phpvms/actions/workflows/build.yml)  [](https://packagist.org/packages/nabeel/phpvms)
|
||||

|
||||
|
||||
phpVMS is a PHP application to run and simulate an airline. It allowed users to register,
|
||||
view flight schedules that you create, and file flight reports, built on the Laravel framework.
|
||||
The latest documentation, with installation instructions is available on the
|
||||
[phpVMS documentation](https://docs.phpvms.net/) site.
|
||||
phpVMS is a PHP application to run and simulate an airline. It allowed users to register, view flight schedules that you create, and file flight reports, built on the Laravel framework. The latest documentation, with installation instructions is available on the [phpVMS documentation](https://docs.phpvms.net/) site.
|
||||
|
||||
## Installation
|
||||
|
||||
A full distribution, with all the composer dependencies, is available at this
|
||||
[GitHub Releases](https://github.com/nabeelio/phpvms/releases) link.
|
||||
A full distribution, with all the composer dependencies, is available at this [GitHub Releases](https://github.com/nabeelio/phpvms/releases) link.
|
||||
|
||||
### Requirements
|
||||
|
||||
@ -24,41 +20,28 @@ A full distribution, with all the composer dependencies, is available at this
|
||||
- tokenizer
|
||||
- intl
|
||||
- zip
|
||||
|
||||
- Database:
|
||||
- MySQL 8.0+ (or MySQL variant, including MariaDB 10.2+ and Percona 8.0+)
|
||||
- PostgreSQL 16+
|
||||
- MySQL 8.0+
|
||||
- MariaDB 10.6+
|
||||
- Redis 6.0+ (optional, recommended)
|
||||
|
||||
[View more details on requirements](https://docs.phpvms.net/requirements)
|
||||
|
||||
### Installer
|
||||
|
||||
1. Upload to your server
|
||||
2. Visit the site, and follow the link to the installer
|
||||
|
||||
[View installation details](https://docs.phpvms.net/installation)
|
||||
|
||||
## Production Deployment with Docker
|
||||
|
||||
The reference production stack is `compose.deploy.yml`. It runs the official
|
||||
phpVMS image (built from the repo `Dockerfile`, based on
|
||||
`serversideup/php:8.5-frankenphp`) with Laravel Octane worker mode enabled,
|
||||
plus MariaDB and Redis. No separate web-server sidecar is needed — FrankenPHP
|
||||
serves HTTP/HTTPS directly.
|
||||
The reference production stack is `compose.deploy.yml`. It runs the official phpvms image (built from the repo `Dockerfile`, based on `serversideup/php:8.5-frankenphp`) with Laravel Octane worker mode enabled, plus PostgreSQL and Redis. No separate web-server sidecar is needed — FrankenPHP serves HTTP/HTTPS directly.
|
||||
|
||||
```bash
|
||||
```shellscript
|
||||
docker compose -f compose.deploy.yml up -d
|
||||
```
|
||||
|
||||
### Octane worker mode
|
||||
|
||||
The image itself ships with serversideup's default classic FrankenPHP entry
|
||||
point (one PHP worker per request). `compose.deploy.yml` opts into Laravel
|
||||
Octane worker mode via a `command:` override on the `app` service — the
|
||||
[pattern documented upstream](https://serversideup.net/open-source/docker-php/docs/framework-guides/laravel/octane).
|
||||
The image itself ships with serversideup's default classic FrankenPHP entry point (one PHP worker per request). `compose.deploy.yml` opts into Laravel Octane worker mode via a `command:` override on the `app` service — the [pattern documented upstream](https://serversideup.net/open-source/docker-php/docs/framework-guides/laravel/octane).
|
||||
|
||||
To fall back to classic FrankenPHP + PHP-worker mode (matches the prior
|
||||
PHP-FPM request semantics — slower per request but bulletproof if a
|
||||
worker-mode bug hits), delete the `command:` line from the `app` service
|
||||
and recreate the container:
|
||||
To fall back to classic FrankenPHP + PHP-worker mode (matches the prior PHP-FPM request semantics — slower per request but bulletproof if a worker-mode bug hits), delete the `command:` line from the `app` service and recreate the container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@ -66,24 +49,21 @@ services:
|
||||
# command: [...octane:start...] # remove this line
|
||||
```
|
||||
|
||||
Under worker mode, `KvpService` (the JSON-backed key/value store used for
|
||||
non-hot settings) is eventually consistent: writes from one worker
|
||||
propagate to other workers on their next read of the same key. If your
|
||||
deployment requires strong consistency for KVP, run with `--workers=1` or
|
||||
remove the `command:` override.
|
||||
Module authors: under Octane the framework stays booted across requests, so avoid per-request data in singleton-bound services, `static` array accumulators, and `boot()`-time toggles without a matching per-request reset. See `app/Http/Middleware/DisableActivityLoggingByDefault.php` for the pattern used to keep activity logging request-scoped.
|
||||
|
||||
Module authors: under Octane the framework stays booted across requests,
|
||||
so avoid per-request data in singleton-bound services, `static` array
|
||||
accumulators, and `boot()`-time toggles without a matching per-request
|
||||
reset. See `app/Http/Middleware/DisableActivityLoggingByDefault.php` for
|
||||
the pattern used to keep activity logging request-scoped.
|
||||
## Development Environment
|
||||
|
||||
## Development Environment with Docker
|
||||
The development environment uses mise. You can read about mise and install it from [here](https://mise.jdx.dev/). If you want mise to handle installing the correct PHP version, you can run:
|
||||
|
||||
A full development environment can be brought up using Docker and
|
||||
[Laravel Sail](https://laravel.com/docs/10.x/sail), without having to install composer/npm locally
|
||||
```shellscript
|
||||
mise install
|
||||
```
|
||||
|
||||
```bash
|
||||
If you install the `mise` shell script integration, the correct PHP version will automatically be selected for you.~~~~
|
||||
|
||||
A full development environment can be brought up using Docker and [Laravel Sail](https://laravel.com/docs/10.x/sail), without having to install composer/npm locally
|
||||
|
||||
```shellscript
|
||||
make docker-test
|
||||
|
||||
# **OR** with docker directly
|
||||
@ -101,16 +81,15 @@ docker run --rm \
|
||||
|
||||
Then go to `http://localhost`.
|
||||
|
||||
Instead of repeatedly typing vendor/bin/sail to execute Sail commands, you may wish to configure a
|
||||
shell alias that allows you to execute Sail's commands more easily:
|
||||
Instead of repeatedly typing vendor/bin/sail to execute Sail commands, you may wish to configure a shell alias that allows you to execute Sail's commands more easily:
|
||||
|
||||
```bash
|
||||
```shellscript
|
||||
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'
|
||||
```
|
||||
|
||||
Then you can execute php, artisan, composer, npm, etc. commands using the sail prefix:
|
||||
|
||||
```bash
|
||||
```shellscript
|
||||
# PHP commands within Laravel Sail...
|
||||
sail php --version
|
||||
|
||||
@ -130,7 +109,7 @@ To interact with databases (MariaDB, Redis...), please refer to the Laravel Sail
|
||||
|
||||
Yarn is required, run:
|
||||
|
||||
```bash
|
||||
```shellscript
|
||||
make build-assets
|
||||
```
|
||||
|
||||
@ -138,10 +117,9 @@ This will build all the assets according to the webpack file.
|
||||
|
||||
### Laravel Boost
|
||||
|
||||
If you want to use AI agents for your development workflow, please ensure you install
|
||||
[Laravel Boost](https://laravel.com/ai/boost) by running the following command:
|
||||
If you want to use AI agents for your development workflow, please ensure you install [Laravel Boost](https://laravel.com/ai/boost) by running the following command:
|
||||
|
||||
```bash
|
||||
```shellscript
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
@ -150,7 +128,3 @@ php artisan boost:install
|
||||
## Contributors
|
||||
|
||||
Thank you to everyone who've contributed to phpVMS!
|
||||
|
||||
<a href="https://github.com/phpvms/phpvms/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=phpvms/phpvms" alt="contributors images"/>
|
||||
</a>
|
||||
|
||||
@ -27,17 +27,14 @@ class CarbonCast implements CastsAttributes
|
||||
return new Carbon($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the attribute to its underlying model values.
|
||||
*
|
||||
* @param Model $model
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
if ($value instanceof Carbon) {
|
||||
return $value->toIso8601ZuluString();
|
||||
return $value->toDateTimeString();
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return (new Carbon($value))->toDateTimeString();
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
@ -173,6 +173,12 @@ class SetVisibleFlights extends Listener
|
||||
*/
|
||||
private static function sqlBool(bool $value): string
|
||||
{
|
||||
$driver = DB::connection()->getDriverName();
|
||||
|
||||
if ($driver === 'pgsql') {
|
||||
return $value ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,5 +10,5 @@ use App\Models\Pirep;
|
||||
|
||||
class AcarsUpdate extends Event
|
||||
{
|
||||
public function __construct(public Pirep $pirep, public Acars $acars) {}
|
||||
public function __construct(public Pirep $pirep, public ?Acars $acars = null) {}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ class FlightImporter extends Importer
|
||||
->rules(['required', 'integer']),
|
||||
|
||||
ImportColumn::make('callsign')
|
||||
->rules(['max:4']),
|
||||
->rules(['max:10']),
|
||||
|
||||
ImportColumn::make('route_code')
|
||||
->rules(['max:5']),
|
||||
@ -125,11 +125,11 @@ class FlightImporter extends Importer
|
||||
|
||||
ImportColumn::make('load_factor')
|
||||
->numeric()
|
||||
->rules(['nullable', 'integer']),
|
||||
->rules(['nullable', 'numeric', 'min:0', 'max:100']),
|
||||
|
||||
ImportColumn::make('load_factor_variance')
|
||||
->numeric()
|
||||
->rules(['nullable', 'integer']),
|
||||
->rules(['nullable', 'numeric', 'min:0', 'max:100']),
|
||||
|
||||
ImportColumn::make('route')
|
||||
->fillRecordUsing(function (Flight $record, ?string $state): void {
|
||||
|
||||
@ -51,7 +51,7 @@ class FlightForm
|
||||
TextInput::make('callsign')
|
||||
->label(__('flights.callsign'))
|
||||
->string()
|
||||
->maxLength(4),
|
||||
->maxLength(10),
|
||||
|
||||
TextInput::make('flight_number')
|
||||
->label(__('flights.flightnumber'))
|
||||
@ -82,10 +82,16 @@ class FlightForm
|
||||
Grid::make()->schema([
|
||||
TextInput::make('load_factor')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->stripCharacters('%')
|
||||
->helperText(__('filament.flight_load_factor_hint')),
|
||||
|
||||
TextInput::make('load_factor_variance')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->stripCharacters('%')
|
||||
->helperText(__('filament.flight_load_factor_variance_hint')),
|
||||
|
||||
])
|
||||
|
||||
@ -18,6 +18,7 @@ class NewsForm
|
||||
->label(__('filament.news_subject'))
|
||||
->string()
|
||||
->required()
|
||||
->maxLength(200)
|
||||
->columnSpanFull(),
|
||||
|
||||
RichEditor::make('body')
|
||||
|
||||
@ -34,6 +34,7 @@ use App\Services\RouteForge\RouteForgeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Backend HTTP entry points for the RouteForge admin tool.
|
||||
@ -125,10 +126,8 @@ final class RouteForgeController extends Controller
|
||||
$query = FlightBundle::query()->orderBy('name');
|
||||
|
||||
if (is_string($search) && $search !== '') {
|
||||
// SQLite + MySQL both fold LIKE case-insensitively for ASCII; the
|
||||
// picker payload is name-keyed so non-ASCII picker UX is a non-issue
|
||||
// at the VA scale this is sized for.
|
||||
$query->where('name', 'like', '%'.$search.'%');
|
||||
$like = DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
|
||||
$query->where('name', $like, '%'.$search.'%');
|
||||
}
|
||||
|
||||
$paginated = $query->paginate($perPage);
|
||||
|
||||
@ -20,6 +20,7 @@ use DateTime;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
@ -168,15 +169,16 @@ class AcarsController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
if (!empty($position['id'])) {
|
||||
Acars::updateOrInsert(
|
||||
['id' => $position['id']],
|
||||
$position
|
||||
);
|
||||
} else {
|
||||
$update = Acars::create($position);
|
||||
$update->save();
|
||||
}
|
||||
DB::transaction(function () use ($position): void {
|
||||
if (!empty($position['id'])) {
|
||||
Acars::updateOrInsert(
|
||||
['id' => $position['id']],
|
||||
$position
|
||||
);
|
||||
} else {
|
||||
Acars::create($position);
|
||||
}
|
||||
});
|
||||
|
||||
$count++;
|
||||
} catch (QueryException $ex) {
|
||||
@ -229,15 +231,16 @@ class AcarsController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
if (isset($log['id'])) {
|
||||
Acars::updateOrInsert(
|
||||
['id' => $log['id']],
|
||||
$log
|
||||
);
|
||||
} else {
|
||||
$acars = Acars::create($log);
|
||||
$acars->save();
|
||||
}
|
||||
DB::transaction(function () use ($log): void {
|
||||
if (isset($log['id'])) {
|
||||
Acars::updateOrInsert(
|
||||
['id' => $log['id']],
|
||||
$log
|
||||
);
|
||||
} else {
|
||||
Acars::create($log);
|
||||
}
|
||||
});
|
||||
|
||||
$count++;
|
||||
} catch (QueryException $ex) {
|
||||
@ -281,15 +284,16 @@ class AcarsController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
if (isset($log['id'])) {
|
||||
Acars::updateOrInsert(
|
||||
['id' => $log['id']],
|
||||
$log
|
||||
);
|
||||
} else {
|
||||
$acars = Acars::create($log);
|
||||
$acars->save();
|
||||
}
|
||||
DB::transaction(function () use ($log): void {
|
||||
if (isset($log['id'])) {
|
||||
Acars::updateOrInsert(
|
||||
['id' => $log['id']],
|
||||
$log
|
||||
);
|
||||
} else {
|
||||
Acars::create($log);
|
||||
}
|
||||
});
|
||||
|
||||
$count++;
|
||||
} catch (QueryException $ex) {
|
||||
|
||||
@ -68,7 +68,7 @@ class LoginController extends Controller
|
||||
*/
|
||||
if (str_contains((string) $id_field, '@')) {
|
||||
$validations[] = 'email';
|
||||
$this->loginFieldValue = $request->input('email');
|
||||
$this->loginFieldValue = mb_strtolower(trim((string) $request->input('email')));
|
||||
} else {
|
||||
$validations[] = function ($attr, string $value, $fail) use ($request): void {
|
||||
try {
|
||||
|
||||
@ -112,7 +112,8 @@ class OAuthController extends Controller
|
||||
return redirect(route('frontend.profile.index'));
|
||||
}
|
||||
|
||||
$user = User::where($provider.'_id', $providerUser->getId())->orWhere('email', $providerUser->getEmail())->first();
|
||||
$providerEmail = mb_strtolower(trim((string) $providerUser->getEmail()));
|
||||
$user = User::where($provider.'_id', $providerUser->getId())->orWhere('email', $providerEmail)->first();
|
||||
|
||||
if ($user) {
|
||||
$user->update([
|
||||
|
||||
@ -96,6 +96,10 @@ class RegisterController extends Controller
|
||||
*/
|
||||
protected function validator(array $data): Validator
|
||||
{
|
||||
if (isset($data['email'])) {
|
||||
$data['email'] = mb_strtolower(trim((string) $data['email']));
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email|max:255|unique:users,email',
|
||||
@ -153,6 +157,10 @@ class RegisterController extends Controller
|
||||
abort(403, 'Registrations are disabled');
|
||||
}
|
||||
|
||||
$request->merge([
|
||||
'email' => mb_strtolower(trim((string) $request->input('email'))),
|
||||
]);
|
||||
|
||||
if (setting('general.invite_only_registrations', false)) {
|
||||
if (!$request->has('invite') && !$request->has('invite_token')) {
|
||||
abort(403, 'Registrations are invite only');
|
||||
|
||||
@ -436,7 +436,7 @@ class PirepController extends Controller
|
||||
public function edit(string $id): RedirectResponse|View
|
||||
{
|
||||
/** @var ?Pirep $pirep */
|
||||
$pirep = Pirep::with(['dpt_airport', 'arr_airport', 'alt_airport', 'fares', 'fields'])->find($id);
|
||||
$pirep = Pirep::with(['dpt_airport', 'arr_airport', 'alt_airport', 'fares'])->find($id);
|
||||
|
||||
if (!$pirep) {
|
||||
Flash::error('Pirep not found');
|
||||
|
||||
@ -153,6 +153,10 @@ class ProfileController extends Controller
|
||||
$rules['field_'.$field->slug] = 'required';
|
||||
}
|
||||
|
||||
$request->merge([
|
||||
'email' => mb_strtolower(trim((string) $request->input('email'))),
|
||||
]);
|
||||
|
||||
$validated = $request->validate($rules);
|
||||
|
||||
if (array_key_exists('password', $validated) && $validated['password'] !== null) {
|
||||
|
||||
@ -31,7 +31,7 @@ class SearchAirportsRequest extends FormRequest
|
||||
{
|
||||
public const array SEARCHABLE_FIELDS = ['iata', 'icao', 'name'];
|
||||
|
||||
public const array SEARCHABLE_OPERATORS = ['=', 'like'];
|
||||
public const array SEARCHABLE_OPERATORS = ['=', 'like', 'ilike'];
|
||||
|
||||
public const array SEARCH_MODES = ['substring', 'prefix'];
|
||||
|
||||
|
||||
@ -294,6 +294,7 @@ class Airport extends Model
|
||||
return [
|
||||
'lat' => 'float',
|
||||
'lon' => 'float',
|
||||
'elevation' => 'integer',
|
||||
'hub' => 'boolean',
|
||||
'ground_handling_cost' => 'float',
|
||||
'fuel_100ll_cost' => 'float',
|
||||
|
||||
@ -222,20 +222,21 @@ class Flight extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'flight_number' => 'integer',
|
||||
'days' => 'integer',
|
||||
'level' => 'integer',
|
||||
'distance' => DistanceCast::class,
|
||||
'flight_time' => 'integer',
|
||||
'flight_type' => FlightType::class,
|
||||
'departure_time' => 'datetime:H:i:s',
|
||||
'arrival_time' => 'datetime:H:i:s',
|
||||
'start_date' => 'datetime',
|
||||
'end_date' => 'datetime',
|
||||
'load_factor' => 'double',
|
||||
'load_factor_variance' => 'double',
|
||||
'pilot_pay' => 'float',
|
||||
'has_bid' => 'boolean',
|
||||
'flight_number' => 'integer',
|
||||
'days' => 'integer',
|
||||
'level' => 'integer',
|
||||
'distance' => DistanceCast::class,
|
||||
'flight_time' => 'integer',
|
||||
'flight_type' => FlightType::class,
|
||||
'departure_time' => 'datetime:H:i:s',
|
||||
'arrival_time' => 'datetime:H:i:s',
|
||||
'start_date' => 'datetime',
|
||||
'end_date' => 'datetime',
|
||||
// `load_factor` and `load_factor_variance` double casts are handled by
|
||||
// their Attribute mutators so blank string inputs canonicalize to NULL
|
||||
// rather than causing MySQL strict-mode errors on DECIMAL columns.
|
||||
'pilot_pay' => 'float',
|
||||
'has_bid' => 'boolean',
|
||||
// `route_leg` int cast is handled by the routeLeg() Attribute
|
||||
// mutator so empty / '0' inputs canonicalize to NULL rather than 0.
|
||||
'enabled' => 'boolean',
|
||||
@ -258,7 +259,7 @@ class Flight extends Model
|
||||
/** @noinspection DynamicInvocationViaScopeResolutionInspection */
|
||||
$flights = self::where('enabled', true);
|
||||
foreach ($days as $day) {
|
||||
$flights = $flights->where('days', '&', $day);
|
||||
$flights = $flights->whereRaw('(days & ?) > 0', [$day]);
|
||||
}
|
||||
|
||||
return $flights;
|
||||
@ -379,6 +380,32 @@ class Flight extends Model
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalize `load_factor` — convert blank strings to null
|
||||
* so MySQL strict mode doesn't reject them for the DECIMAL column.
|
||||
* Also handles float casting now that the `double` cast is removed.
|
||||
*/
|
||||
protected function loadFactor(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: static fn (mixed $value): ?float => $value === null ? null : (float) $value,
|
||||
set: static fn (mixed $value): ?float => blank($value) ? null : (float) $value,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalize `load_factor_variance` — convert blank strings to null
|
||||
* so MySQL strict mode doesn't reject them for the DECIMAL column.
|
||||
* Also handles float casting now that the `double` cast is removed.
|
||||
*/
|
||||
protected function loadFactorVariance(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: static fn (mixed $value): ?float => $value === null ? null : (float) $value,
|
||||
set: static fn (mixed $value): ?float => blank($value) ? null : (float) $value,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse null / '' / 0 / '0' to canonical NULL for route-key fields.
|
||||
*
|
||||
|
||||
@ -284,6 +284,17 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, MustVerif
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize email to lowercase so lookups and the unique constraint behave
|
||||
* consistently across case-sensitive (PostgreSQL) and case-insensitive (MySQL) drivers.
|
||||
*/
|
||||
public function email(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: static fn (?string $value): ?string => $value === null ? null : mb_strtolower(trim($value)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a "privatized" version of someones name - First and middle names full, last name initials
|
||||
*/
|
||||
|
||||
@ -10,7 +10,7 @@ use Illuminate\Support\Carbon;
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_field_id
|
||||
* @property string $user_id
|
||||
* @property int $user_id
|
||||
* @property string|null $value
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
@ -42,6 +42,14 @@ class UserFieldValue extends Model
|
||||
|
||||
public static array $rules = [];
|
||||
|
||||
#[\Override]
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return related field's name along with field values
|
||||
*/
|
||||
|
||||
@ -5,6 +5,7 @@ namespace App\Queries;
|
||||
use App\Http\Requests\SearchAirportsRequest;
|
||||
use App\Models\Airport;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Build an Eloquent\Builder for airport listing/search endpoints.
|
||||
@ -31,6 +32,11 @@ class AirportSearchQueryV1
|
||||
{
|
||||
public function __construct(private readonly SearchAirportsRequest $request) {}
|
||||
|
||||
private function likeOperator(): string
|
||||
{
|
||||
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
|
||||
}
|
||||
|
||||
public function build(): Builder
|
||||
{
|
||||
$data = $this->request->validated();
|
||||
@ -114,7 +120,7 @@ class AirportSearchQueryV1
|
||||
*/
|
||||
private function resolveSearchFields(?string $searchFields, array $searchDataKeys): array
|
||||
{
|
||||
$defaultFields = array_fill_keys(SearchAirportsRequest::SEARCHABLE_FIELDS, 'like');
|
||||
$defaultFields = array_fill_keys(SearchAirportsRequest::SEARCHABLE_FIELDS, $this->likeOperator());
|
||||
if ($searchFields === null || $searchFields === '') {
|
||||
return $defaultFields;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ namespace App\Queries;
|
||||
use App\Http\Requests\SearchFlightsRequest;
|
||||
use App\Models\Flight;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Builds the Eloquent query for Flight list/search endpoints.
|
||||
@ -19,14 +20,6 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
*/
|
||||
class FlightSearchQuery
|
||||
{
|
||||
/**
|
||||
* Field-specific search allowlist. Mirrors the old
|
||||
* FlightRepository::$fieldSearchable + RequestCriteria's
|
||||
* `?search=field:value;...` syntax. The 'like' entries match the
|
||||
* legacy LIKE behavior; everything else is exact match.
|
||||
*
|
||||
* @var array<string, 'exact'|'like'>
|
||||
*/
|
||||
private const array FIELD_SEARCH = [
|
||||
'arr_airport_id' => 'exact',
|
||||
'callsign' => 'exact',
|
||||
@ -41,6 +34,11 @@ class FlightSearchQuery
|
||||
'notes' => 'like',
|
||||
];
|
||||
|
||||
private function likeOperator(): string
|
||||
{
|
||||
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
|
||||
}
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
@ -75,12 +73,6 @@ class FlightSearchQuery
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the legacy `?search=field:value;field:value` syntax.
|
||||
* Mirrors PirepSearchQuery / UserSearchQuery from earlier phases.
|
||||
*
|
||||
* @param Builder<Flight> $query
|
||||
*/
|
||||
private function applySearch(Builder $query, SearchFlightsRequest $request): void
|
||||
{
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
@ -88,6 +80,8 @@ class FlightSearchQuery
|
||||
return;
|
||||
}
|
||||
|
||||
$like = $this->likeOperator();
|
||||
|
||||
if (str_contains($search, ':')) {
|
||||
$clauses = [];
|
||||
foreach (explode(';', $search) as $pair) {
|
||||
@ -114,10 +108,10 @@ class FlightSearchQuery
|
||||
}
|
||||
|
||||
if ($clauses !== []) {
|
||||
$query->where(function (Builder $q) use ($clauses): void {
|
||||
$query->where(function (Builder $q) use ($clauses, $like): void {
|
||||
foreach ($clauses as [$field, $value, $mode]) {
|
||||
if ($mode === 'like') {
|
||||
$q->orWhere($field, 'like', '%'.$value.'%');
|
||||
$q->orWhere($field, $like, '%'.$value.'%');
|
||||
} else {
|
||||
$q->orWhere($field, '=', $value);
|
||||
}
|
||||
@ -128,9 +122,9 @@ class FlightSearchQuery
|
||||
}
|
||||
}
|
||||
|
||||
$query->where(function (Builder $q) use ($search): void {
|
||||
$query->where(function (Builder $q) use ($search, $like): void {
|
||||
foreach (self::FREE_TEXT_COLUMNS as $column) {
|
||||
$q->orWhere($column, 'like', '%'.$search.'%');
|
||||
$q->orWhere($column, $like, '%'.$search.'%');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ namespace App\Queries;
|
||||
use App\Http\Requests\SearchPirepsRequest;
|
||||
use App\Models\Pirep;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Builds the Eloquent query for PIREP list endpoints.
|
||||
@ -37,6 +38,11 @@ class PirepSearchQuery
|
||||
*/
|
||||
private const array FREE_TEXT_COLUMNS = ['id', 'flight_number'];
|
||||
|
||||
private function likeOperator(): string
|
||||
{
|
||||
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
|
||||
}
|
||||
|
||||
public function build(SearchPirepsRequest $request): Builder
|
||||
{
|
||||
$query = Pirep::query()
|
||||
@ -92,8 +98,9 @@ class PirepSearchQuery
|
||||
}
|
||||
|
||||
$query->where(function (Builder $q) use ($search): void {
|
||||
$like = $this->likeOperator();
|
||||
foreach (self::FREE_TEXT_COLUMNS as $col) {
|
||||
$q->orWhere($col, 'like', '%'.$search.'%');
|
||||
$q->orWhere($col, $like, '%'.$search.'%');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ use App\Enums\UserState;
|
||||
use App\Http\Requests\SearchUsersRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Builds the Eloquent query for the public pilots list (`/users`).
|
||||
@ -37,6 +38,15 @@ class UserSearchQuery
|
||||
'state' => '=',
|
||||
];
|
||||
|
||||
private function resolveOperator(string $operator): string
|
||||
{
|
||||
if ($operator !== 'like') {
|
||||
return $operator;
|
||||
}
|
||||
|
||||
return $this->likeOperator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Free-text search columns (when search has no `field:` prefix).
|
||||
*
|
||||
@ -44,6 +54,11 @@ class UserSearchQuery
|
||||
*/
|
||||
private const array FREE_TEXT_COLUMNS = ['name', 'email'];
|
||||
|
||||
private function likeOperator(): string
|
||||
{
|
||||
return DB::connection()->getDriverName() === 'pgsql' ? 'ilike' : 'like';
|
||||
}
|
||||
|
||||
public function build(SearchUsersRequest $request): Builder
|
||||
{
|
||||
$query = User::query()
|
||||
@ -109,14 +124,14 @@ class UserSearchQuery
|
||||
continue;
|
||||
}
|
||||
|
||||
$clauses[] = [$field, self::FIELD_SEARCH[$field], $value];
|
||||
$clauses[] = [$field, $this->resolveOperator(self::FIELD_SEARCH[$field]), $value];
|
||||
}
|
||||
|
||||
if ($clauses !== []) {
|
||||
$query->where(function (Builder $q) use ($clauses): void {
|
||||
foreach ($clauses as [$field, $operator, $value]) {
|
||||
if ($operator === 'like') {
|
||||
$q->orWhere($field, 'like', '%'.$value.'%');
|
||||
if (in_array($operator, ['like', 'ilike'], true)) {
|
||||
$q->orWhere($field, $operator, '%'.$value.'%');
|
||||
} else {
|
||||
$q->orWhere($field, '=', $value);
|
||||
}
|
||||
@ -130,9 +145,10 @@ class UserSearchQuery
|
||||
}
|
||||
|
||||
// Free-text: OR across name + email
|
||||
$query->where(function (Builder $q) use ($search): void {
|
||||
$like = $this->likeOperator();
|
||||
$query->where(function (Builder $q) use ($search, $like): void {
|
||||
foreach (self::FREE_TEXT_COLUMNS as $col) {
|
||||
$q->orWhere($col, 'like', '%'.$search.'%');
|
||||
$q->orWhere($col, $like, '%'.$search.'%');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -179,16 +179,32 @@ class FlightService extends Service
|
||||
}
|
||||
|
||||
// Match nullable scalar columns including legacy empty-string values.
|
||||
// Stored values may be NULL, '', or an actual scalar; treat empty as
|
||||
// equivalent to null so casts that coerce '' to 0 (e.g. integer cast
|
||||
// on route_leg) still resolve correctly.
|
||||
foreach (['route_code', 'route_leg', 'days'] as $column) {
|
||||
// route_code is a string column — empty strings are valid on all DBs.
|
||||
// route_leg and days are integer columns — PG cannot compare int to '',
|
||||
// so only check null and 0.
|
||||
$stringColumns = ['route_code'];
|
||||
$integerColumns = ['route_leg', 'days'];
|
||||
|
||||
foreach ($stringColumns as $column) {
|
||||
$value = $flight->{$column};
|
||||
|
||||
if (in_array($value, [null, '', '0'], true)) {
|
||||
$query->where(function ($q) use ($column): void {
|
||||
$q->whereNull($column)
|
||||
->orWhere($column, '')
|
||||
->orWhere($column, 0);
|
||||
});
|
||||
} else {
|
||||
$query->where($column, $value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($integerColumns as $column) {
|
||||
$value = $flight->{$column};
|
||||
|
||||
if (in_array($value, [null, '', 0, '0'], true)) {
|
||||
$query->where(function ($q) use ($column): void {
|
||||
$q->whereNull($column)
|
||||
->orWhere($column, '')
|
||||
->orWhere($column, 0);
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -58,6 +58,10 @@ class AirportImporter extends ImportExport
|
||||
$row['fuel_jeta_cost'] = (float) $row['fuel_jeta_cost'];
|
||||
}
|
||||
|
||||
$row['elevation'] = is_numeric($row['elevation'] ?? '') ? (int) $row['elevation'] : null;
|
||||
$row['fuel_100ll_cost'] = is_numeric($row['fuel_100ll_cost'] ?? '') ? (float) $row['fuel_100ll_cost'] : null;
|
||||
$row['fuel_mogas_cost'] = is_numeric($row['fuel_mogas_cost'] ?? '') ? (float) $row['fuel_mogas_cost'] : null;
|
||||
|
||||
try {
|
||||
Airport::updateOrCreate([
|
||||
'id' => $row['icao'],
|
||||
|
||||
@ -46,8 +46,8 @@ class FlightImporter extends ImportExport
|
||||
'distance' => 'nullable|numeric',
|
||||
'flight_time' => 'required|integer',
|
||||
'flight_type' => 'required|alpha',
|
||||
'load_factor' => 'nullable',
|
||||
'load_factor_variance' => 'nullable',
|
||||
'load_factor' => 'nullable|numeric|min:0|max:100',
|
||||
'load_factor_variance' => 'nullable|numeric|min:0|max:100',
|
||||
'pilot_pay' => 'nullable',
|
||||
'route' => 'nullable',
|
||||
'notes' => 'nullable',
|
||||
@ -226,9 +226,9 @@ class FlightImporter extends ImportExport
|
||||
$this->processAirport($row['alt_airport']);
|
||||
}
|
||||
|
||||
$this->processSubfleets($flight, $row['subfleets']);
|
||||
$this->processFares($flight, $row['fares']);
|
||||
$this->processFields($flight, $row['fields']);
|
||||
$this->processSubfleets($flight, $row['subfleets'] ?? '');
|
||||
$this->processFares($flight, $row['fares'] ?? '');
|
||||
$this->processFields($flight, $row['fields'] ?? '');
|
||||
|
||||
$this->log('Imported row '.($index + 1));
|
||||
|
||||
|
||||
@ -70,8 +70,8 @@ class SubfleetImporter extends ImportExport
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->processFares($subfleet, $row['fares']);
|
||||
$this->processRanks($subfleet, $row['ranks']);
|
||||
$this->processFares($subfleet, $row['fares'] ?? '');
|
||||
$this->processRanks($subfleet, $row['ranks'] ?? '');
|
||||
|
||||
$this->log('Imported '.$row['type']);
|
||||
|
||||
|
||||
@ -85,10 +85,11 @@ class ImportService extends Service
|
||||
$records = $reader->getRecords($header_rows);
|
||||
foreach ($records as $offset => $row) {
|
||||
// turn it into a collection and run some filtering
|
||||
$row = collect($row)->map(function ($val, $index): string {
|
||||
$row = collect($row)->map(function ($val, $index): ?string {
|
||||
$val = trim($val);
|
||||
$val = str_ireplace(['\\n', '\\r'], '', $val);
|
||||
|
||||
return str_ireplace(['\\n', '\\r'], '', $val);
|
||||
return $val === '' ? null : $val;
|
||||
})->toArray();
|
||||
|
||||
// Try to validate
|
||||
|
||||
@ -5,190 +5,26 @@ declare(strict_types=1);
|
||||
namespace App\Services\Installer;
|
||||
|
||||
use App\Contracts\Service;
|
||||
use App\Models\Setting;
|
||||
use App\Services\DatabaseService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
use function trim;
|
||||
use Database\Seeders\BaseDataSeeder;
|
||||
use Database\Seeders\SettingsSeeder;
|
||||
|
||||
class SeederService extends Service
|
||||
{
|
||||
private array $counters = [];
|
||||
|
||||
private array $offsets = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly DatabaseService $databaseSvc
|
||||
) {}
|
||||
/**
|
||||
* Synchronize all the seed files, run this after the migrations
|
||||
* and on first install.
|
||||
*/
|
||||
public function syncAllSeeds(): void
|
||||
{
|
||||
app(BaseDataSeeder::class)->run();
|
||||
app(SettingsSeeder::class)->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* See if there are any seeds that are out of sync
|
||||
*/
|
||||
public function seedsPending(): bool
|
||||
{
|
||||
return $this->settingsSeedsPending();
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncronize all the seed files, run this after the migrations
|
||||
* and on first install.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function syncAllSeeds(): void
|
||||
{
|
||||
$this->syncAllSettings();
|
||||
|
||||
// Seed base
|
||||
$this->databaseSvc->seedFromYamlFile(database_path('seeders/base/base.yml'));
|
||||
}
|
||||
|
||||
public function syncAllSettings(): void
|
||||
{
|
||||
$data = file_get_contents(database_path('/seeders/base/settings.yml'));
|
||||
$yml = Yaml::parse($data);
|
||||
foreach ($yml as $setting) {
|
||||
if (trim((string) $setting['key']) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addSetting($setting['key'], $setting);
|
||||
}
|
||||
}
|
||||
|
||||
public function addSetting($key, $attrs): void
|
||||
{
|
||||
$id = Setting::formatKey($key);
|
||||
$group = $attrs['group'];
|
||||
$order = $this->getNextOrderNumber($group);
|
||||
|
||||
$attrs = array_merge(
|
||||
[
|
||||
'id' => $id,
|
||||
'key' => $key,
|
||||
'offset' => $this->offsets[$group],
|
||||
'order' => $order,
|
||||
'name' => '',
|
||||
'group' => $group,
|
||||
'value' => $attrs['value'],
|
||||
'default' => $attrs['value'],
|
||||
'options' => '',
|
||||
'type' => 'hidden',
|
||||
'description' => '',
|
||||
],
|
||||
$attrs
|
||||
);
|
||||
|
||||
$count = DB::table('settings')->where('id', $id)->count('id');
|
||||
if ($count === 0) {
|
||||
DB::table('settings')->insert($attrs);
|
||||
} else {
|
||||
unset($attrs['value']); // Don't overwrite this
|
||||
DB::table('settings')
|
||||
->where('id', $id)
|
||||
->update($attrs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically figure out the offset and the start number for a group.
|
||||
* This way we don't need to mess with how to order things
|
||||
* When calling getNextOrderNumber(users) 31, will be returned, then 32, and so on
|
||||
*/
|
||||
private function addCounterGroup(string $name, ?int $offset = null, int $start_offset = 0): void
|
||||
{
|
||||
if ($offset === null) {
|
||||
$group = DB::table('settings')
|
||||
->where('group', $name)
|
||||
->first();
|
||||
|
||||
if ($group === null) {
|
||||
$offset = DB::table('settings')->max('offset');
|
||||
if ($offset === null) {
|
||||
$offset = 0;
|
||||
$start_offset = 1;
|
||||
} else {
|
||||
$offset = (int) $offset;
|
||||
$offset += 100;
|
||||
$start_offset = $offset + 1;
|
||||
}
|
||||
} else {
|
||||
// Now find the number to start from
|
||||
$start_offset = DB::table('settings')->where('group', $name)->max('order');
|
||||
if ($start_offset === null) {
|
||||
$start_offset = $offset + 1;
|
||||
} else {
|
||||
$start_offset = (int) $start_offset;
|
||||
$start_offset++;
|
||||
}
|
||||
|
||||
$offset = $group->offset;
|
||||
}
|
||||
}
|
||||
|
||||
$this->counters[$name] = $start_offset;
|
||||
$this->offsets[$name] = $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next increment number from a group
|
||||
*/
|
||||
private function getNextOrderNumber($group): int
|
||||
{
|
||||
if (!\in_array($group, $this->counters, true)) {
|
||||
$this->addCounterGroup($group);
|
||||
}
|
||||
|
||||
$idx = $this->counters[$group];
|
||||
$this->counters[$group]++;
|
||||
|
||||
return $idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* See if there are seeds pending for the settings
|
||||
*/
|
||||
private function settingsSeedsPending(): bool
|
||||
{
|
||||
$all_settings = DB::table('settings')->get();
|
||||
$data = file_get_contents(database_path('/seeders/settings.yml'));
|
||||
$yml = Yaml::parse($data);
|
||||
|
||||
// See if any are missing from the DB
|
||||
foreach ($yml as $setting) {
|
||||
if (trim((string) $setting['key']) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = Setting::formatKey($setting['key']);
|
||||
$row = $all_settings->firstWhere('id', $id);
|
||||
|
||||
// Doesn't exist in the table, quit early and say there is stuff pending
|
||||
if (!$row) {
|
||||
Log::info('Setting '.$id.' missing, update available');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// See if any of these column values have changed
|
||||
foreach (['name', 'description'] as $column) {
|
||||
$currVal = $row->{$column};
|
||||
$newVal = $setting[$column];
|
||||
if ($currVal !== $newVal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// See if any of the options have changed
|
||||
if ($row->type === 'select' && (!empty($row->options) && $row->options !== $setting['options'])) {
|
||||
Log::info('Options for '.$id.' changed, update available');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return (new SettingsSeeder())->settingsPending();
|
||||
}
|
||||
}
|
||||
|
||||
@ -443,9 +443,9 @@ class PirepService extends Service
|
||||
|
||||
// If pirep is still at PENDING or DRAFT state decide the default behavior by looking at rank settings
|
||||
if ($pirep->state === PirepState::PENDING || $pirep->state === PirepState::DRAFT) {
|
||||
if ($pirep->source === PirepSource::ACARS && $pirep->user->rank->auto_approve_acars) {
|
||||
if ($pirep->source === PirepSource::ACARS && $pirep->user->rank?->auto_approve_acars) {
|
||||
$default_state = PirepState::ACCEPTED;
|
||||
} elseif ($pirep->source === PirepSource::MANUAL && $pirep->user->rank->auto_approve_manual) {
|
||||
} elseif ($pirep->source === PirepSource::MANUAL && $pirep->user->rank?->auto_approve_manual) {
|
||||
$default_state = PirepState::ACCEPTED;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@ -208,12 +209,15 @@ class UserService extends Service
|
||||
return $user;
|
||||
}
|
||||
|
||||
$user->pilot_id = $this->getNextAvailablePilotId();
|
||||
$user->save();
|
||||
return DB::transaction(function () use ($user): User {
|
||||
$maxPilotId = (int) User::withTrashed()->max('pilot_id');
|
||||
$user->pilot_id = $maxPilotId + 1;
|
||||
$user->save();
|
||||
|
||||
Log::info('Set pilot ID for user '.$user->id.' to '.$user->pilot_id);
|
||||
Log::info('Set pilot ID for user '.$user->id.' to '.$user->pilot_id);
|
||||
|
||||
return $user;
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -236,19 +240,21 @@ class UserService extends Service
|
||||
return $user;
|
||||
}
|
||||
|
||||
if ($this->isPilotIdAlreadyUsed($pilot_id)) {
|
||||
Log::error('User with id '.$pilot_id.' already exists');
|
||||
return DB::transaction(function () use ($user, $pilot_id): User {
|
||||
if (User::where('pilot_id', '=', $pilot_id)->exists()) {
|
||||
Log::error('User with id '.$pilot_id.' already exists');
|
||||
|
||||
throw new UserPilotIdExists($user);
|
||||
}
|
||||
throw new UserPilotIdExists($user);
|
||||
}
|
||||
|
||||
$old_id = $user->pilot_id;
|
||||
$user->pilot_id = $pilot_id;
|
||||
$user->save();
|
||||
$old_id = $user->pilot_id;
|
||||
$user->pilot_id = $pilot_id;
|
||||
$user->save();
|
||||
|
||||
Log::info('Changed pilot ID for user '.$user->id.' from '.$old_id.' to '.$user->pilot_id);
|
||||
Log::info('Changed pilot ID for user '.$user->id.' from '.$old_id.' to '.$user->pilot_id);
|
||||
|
||||
return $user;
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -268,12 +274,12 @@ class UserService extends Service
|
||||
|
||||
/** @var Airline $airline */
|
||||
foreach ($airlines as $airline) {
|
||||
if (str_contains($pilot_id, $airline->icao)) {
|
||||
if (str_starts_with($pilot_id, $airline->icao)) {
|
||||
$ident_str = $airline->icao;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!empty($airline->iata) && str_contains($pilot_id, (string) $airline->iata)) {
|
||||
if (!empty($airline->iata) && str_starts_with($pilot_id, (string) $airline->iata)) {
|
||||
$ident_str = $airline->iata;
|
||||
break;
|
||||
}
|
||||
@ -284,6 +290,10 @@ class UserService extends Service
|
||||
}
|
||||
|
||||
$parsed_pilot_id = str_replace($ident_str, '', $pilot_id);
|
||||
if ($parsed_pilot_id === '' || !ctype_digit($parsed_pilot_id)) {
|
||||
throw new PilotIdNotFound($pilot_id);
|
||||
}
|
||||
|
||||
$user = User::where(['airline_id' => $airline->id, 'pilot_id' => $parsed_pilot_id])->first();
|
||||
if (empty($user)) {
|
||||
throw new PilotIdNotFound($pilot_id);
|
||||
|
||||
@ -10,10 +10,11 @@ use Exception;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class DatabaseService extends Service
|
||||
class YamlDatabaseService extends Service
|
||||
{
|
||||
protected array $uuidTables = [
|
||||
'acars',
|
||||
@ -21,6 +22,15 @@ class DatabaseService extends Service
|
||||
'pireps',
|
||||
];
|
||||
|
||||
protected array $datetimeTimeColumns = [
|
||||
'arrival_time',
|
||||
'block_off_time',
|
||||
'block_on_time',
|
||||
'departure_time',
|
||||
'landing_time',
|
||||
'post_date',
|
||||
];
|
||||
|
||||
protected function time(): string
|
||||
{
|
||||
return (string) Carbon::now('UTC'); // ->format('Y-m-d H:i:s');
|
||||
@ -88,6 +98,8 @@ class DatabaseService extends Service
|
||||
|
||||
$imported[$table]++;
|
||||
}
|
||||
|
||||
$this->resetPostgresSequence($table, $id_column);
|
||||
}
|
||||
|
||||
return $imported;
|
||||
@ -119,8 +131,20 @@ class DatabaseService extends Service
|
||||
|
||||
// if any time fields are == to "now", then insert the right time
|
||||
foreach ($row as $column => $value) {
|
||||
if (!empty($value) && strtolower((string) $value) === 'now') {
|
||||
$row[$column] = Carbon::now('UTC');
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isDateTimeColumn = str_ends_with((string) $column, '_at')
|
||||
|| in_array($column, $this->datetimeTimeColumns, true);
|
||||
if (!$isDateTimeColumn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strtolower((string) $value) === 'now') {
|
||||
$row[$column] = Carbon::now('UTC')->toDateTimeString();
|
||||
} else {
|
||||
$row[$column] = Carbon::parse($value)->toDateTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,29 +153,33 @@ class DatabaseService extends Service
|
||||
$count = DB::table($table)->where($id_col, $row[$id_col])->count($id_col);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($count > 0) {
|
||||
if ($ignore_if_exists) {
|
||||
return $row;
|
||||
if ($count > 0 && $ignore_if_exists) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
foreach ($ignore_on_updates as $ignore_column) {
|
||||
if (array_key_exists($ignore_column, $row)) {
|
||||
unset($row[$ignore_column]);
|
||||
}
|
||||
|
||||
foreach ($ignore_on_updates as $ignore_column) {
|
||||
if (array_key_exists($ignore_column, $row)) {
|
||||
unset($row[$ignore_column]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::table($table)
|
||||
->where($id_col, $row[$id_col])
|
||||
->update($row);
|
||||
} else {
|
||||
// Remove ID column if it exists and its empty, let the DB set it
|
||||
/*if (array_key_exists($id_col, $row) && empty($row[$id_col])) {
|
||||
unset($row[$id_col]);
|
||||
}*/
|
||||
|
||||
DB::table($table)->insert($row);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Run the write inside a (possibly nested) transaction so a failure
|
||||
// rolls back to a SAVEPOINT instead of poisoning the surrounding
|
||||
// transaction. On PostgreSQL any failed statement aborts the whole
|
||||
// transaction, so swallowing the exception without a savepoint would
|
||||
// leave every later query failing with "current transaction is aborted".
|
||||
DB::transaction(function () use ($count, $id_col, $table, $row): void {
|
||||
if ($count > 0) {
|
||||
DB::table($table)
|
||||
->where($id_col, $row[$id_col])
|
||||
->update($row);
|
||||
} else {
|
||||
DB::table($table)->insert($row);
|
||||
}
|
||||
});
|
||||
} catch (QueryException $queryException) {
|
||||
Log::error('Error while running query: '.$queryException->getMessage(), ['exception' => $queryException]);
|
||||
if (!$ignore_errors) {
|
||||
@ -161,4 +189,33 @@ class DatabaseService extends Service
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
protected function resetPostgresSequence(string $table, string $idColumn = 'id'): void
|
||||
{
|
||||
if (DB::getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
$fullTable = DB::getTablePrefix().$table;
|
||||
|
||||
// Guard every step with a query that cannot fail, because on PostgreSQL
|
||||
// a failed statement aborts the entire surrounding transaction. The old
|
||||
// implementation relied on catching QueryException, but by then the
|
||||
// transaction was already poisoned and every later query in the same
|
||||
// test transaction failed with "current transaction is aborted".
|
||||
if (!Schema::hasColumn($table, $idColumn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Returns null for pivot tables and uuid/string primary keys that have
|
||||
// no owned sequence, so we skip setval entirely instead of targeting a
|
||||
// non-existent "<table>_<id>_seq" relation.
|
||||
$sequence = DB::scalar('SELECT pg_get_serial_sequence(?, ?)', [$fullTable, $idColumn]);
|
||||
|
||||
if ($sequence === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::statement(sprintf("SELECT setval('%s', COALESCE((SELECT MAX(%s) FROM %s), 1))", $sequence, $idColumn, $fullTable));
|
||||
}
|
||||
}
|
||||
@ -1,55 +1,59 @@
|
||||
services:
|
||||
laravel.test:
|
||||
build:
|
||||
context: ./vendor/laravel/sail/runtimes/8.5
|
||||
context: "./vendor/laravel/sail/runtimes/8.5"
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
WWWGROUP: "${WWWGROUP}"
|
||||
image: sail-8.5/app
|
||||
image: "sail-8.5/app"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "${APP_PORT:-80}:80"
|
||||
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}"
|
||||
#env_file:
|
||||
# - .env
|
||||
environment:
|
||||
WWWUSER: "${WWWUSER}"
|
||||
LARAVEL_SAIL: 1
|
||||
XDEBUG_MODE: "${SAIL_XDEBUG_MODE:-off}"
|
||||
XDEBUG_CONFIG: "${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}"
|
||||
IGNITION_LOCAL_SITES_PATH: "${PWD}"
|
||||
DB_CONNECTION: pgsql
|
||||
DB_URL: postgres://phpvms:password@pgsql:5432/phpvms
|
||||
DB_HOST: pgsql
|
||||
DB_PORT: 5432
|
||||
DB_DATABASE: phpvms
|
||||
DB_USERNAME: phpvms
|
||||
DB_PASSWORD: password
|
||||
volumes:
|
||||
- ".:/var/www/html"
|
||||
networks:
|
||||
- sail
|
||||
depends_on:
|
||||
- mysql
|
||||
- pgsql
|
||||
- redis
|
||||
- mailpit
|
||||
mysql:
|
||||
image: "mysql:8.4"
|
||||
pgsql:
|
||||
image: "postgres:18-alpine"
|
||||
ports:
|
||||
- "${FORWARD_DB_PORT:-3306}:3306"
|
||||
- "${FORWARD_DB_PORT:-5432}:5432"
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_ROOT_HOST: "%"
|
||||
MYSQL_DATABASE: "${DB_DATABASE}"
|
||||
MYSQL_USER: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||
MYSQL_EXTRA_OPTIONS: "${MYSQL_EXTRA_OPTIONS:-}"
|
||||
POSTGRES_DB: phpvms
|
||||
POSTGRES_USER: phpvms
|
||||
POSTGRES_PASSWORD: password
|
||||
volumes:
|
||||
- "sail-mysql:/var/lib/mysql"
|
||||
- "./vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh"
|
||||
- "sail-pgsql:/var/lib/postgresql"
|
||||
- "./vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql"
|
||||
networks:
|
||||
- sail
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- mysqladmin
|
||||
- ping
|
||||
- "-p${DB_PASSWORD}"
|
||||
- pg_isready
|
||||
- "-q"
|
||||
- "-d"
|
||||
- "phpvms"
|
||||
- "-U"
|
||||
- "phpvms"
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
redis:
|
||||
@ -74,10 +78,12 @@ services:
|
||||
- "${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025"
|
||||
networks:
|
||||
- sail
|
||||
|
||||
networks:
|
||||
sail:
|
||||
driver: bridge
|
||||
volumes:
|
||||
sail-mysql:
|
||||
sail-pgsql:
|
||||
driver: local
|
||||
sail-redis:
|
||||
driver: local
|
||||
|
||||
39
compose.test.yml
Normal file
39
compose.test.yml
Normal file
@ -0,0 +1,39 @@
|
||||
name: phpvms-test
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
ports:
|
||||
- "13306:3306"
|
||||
environment:
|
||||
MYSQL_DATABASE: testing
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "--password=password"]
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
start_period: 15s
|
||||
|
||||
pgsql:
|
||||
image: postgres:18-alpine
|
||||
ports:
|
||||
- "15432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: testing
|
||||
POSTGRES_USER: phpvms
|
||||
POSTGRES_PASSWORD: password
|
||||
volumes:
|
||||
- pgsql-data:/var/lib/postgresql
|
||||
- ./vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql:ro
|
||||
- ./resources/docker/pgsql/01-test-user.sql:/docker-entrypoint-initdb.d/01-test_user.sql:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-q", "-d", "testing", "-U", "phpvms"]
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
pgsql-data:
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "phpvms/phpvms",
|
||||
"description": "phpVMS - Virtual Airline Administration",
|
||||
"version": "8.0",
|
||||
"keywords": [
|
||||
"phpvms",
|
||||
"virtual",
|
||||
@ -27,7 +28,6 @@
|
||||
"ext-intl": "*",
|
||||
"ext-zip": "*",
|
||||
"fisharebest/ext-calendar": "^2.5",
|
||||
"symfony/flex": "^2.4.1",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-intl-icu": "*",
|
||||
"symfony/polyfill-intl-idn": "*",
|
||||
@ -117,7 +117,8 @@
|
||||
"larastan/larastan": "^3.9.6",
|
||||
"fruitcake/laravel-debugbar": "^4.2.8",
|
||||
"laravel/boost": "^2.0",
|
||||
"laravel/tinker": "^3.0"
|
||||
"laravel/tinker": "^3.0",
|
||||
"carthage-software/mago": "^1.29.0"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
||||
383
composer.lock
generated
383
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "00a62aaf5600d5d6decb467db19b7d18",
|
||||
"content-hash": "231d07f2838be7c38ff05489237f22e7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "akaunting/laravel-money",
|
||||
@ -198,16 +198,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.382.2",
|
||||
"version": "3.383.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "6844cc6421c47d6b96633ab8039045012acbeb27"
|
||||
"reference": "11c2de39e4511dc99e44f049c7dfc8087e051867"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6844cc6421c47d6b96633ab8039045012acbeb27",
|
||||
"reference": "6844cc6421c47d6b96633ab8039045012acbeb27",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/11c2de39e4511dc99e44f049c7dfc8087e051867",
|
||||
"reference": "11c2de39e4511dc99e44f049c7dfc8087e051867",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -289,9 +289,9 @@
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.382.2"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.383.2"
|
||||
},
|
||||
"time": "2026-05-27T18:11:41+00:00"
|
||||
"time": "2026-06-01T18:08:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "beberlei/assert",
|
||||
@ -1183,16 +1183,16 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/composer",
|
||||
"version": "2.9.8",
|
||||
"version": "2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/composer.git",
|
||||
"reference": "39ee8baff8e97a1b657bbfcd6a236ff93a5efbb2"
|
||||
"reference": "c13824d95608b15913a7c0def0a3dea4474b71fc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/composer/zipball/39ee8baff8e97a1b657bbfcd6a236ff93a5efbb2",
|
||||
"reference": "39ee8baff8e97a1b657bbfcd6a236ff93a5efbb2",
|
||||
"url": "https://api.github.com/repos/composer/composer/zipball/c13824d95608b15913a7c0def0a3dea4474b71fc",
|
||||
"reference": "c13824d95608b15913a7c0def0a3dea4474b71fc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1245,7 +1245,7 @@
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "2.9-dev"
|
||||
"dev-main": "2.10-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -1280,7 +1280,7 @@
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/composer/issues",
|
||||
"security": "https://github.com/composer/composer/security/policy",
|
||||
"source": "https://github.com/composer/composer/tree/2.9.8"
|
||||
"source": "https://github.com/composer/composer/tree/2.10.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -1292,7 +1292,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-13T07:28:38+00:00"
|
||||
"time": "2026-05-28T09:22:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/installers",
|
||||
@ -3285,25 +3285,26 @@
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.10.5",
|
||||
"version": "7.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/guzzle.git",
|
||||
"reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148"
|
||||
"reference": "c987f8ce84b8434fa430795eca0f3430663da72b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148",
|
||||
"reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/c987f8ce84b8434fa430795eca0f3430663da72b",
|
||||
"reference": "c987f8ce84b8434fa430795eca0f3430663da72b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/promises": "^2.3",
|
||||
"guzzlehttp/psr7": "^2.8",
|
||||
"guzzlehttp/promises": "^2.5",
|
||||
"guzzlehttp/psr7": "^2.11",
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/deprecation-contracts": "^2.2 || ^3.0"
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-client-implementation": "1.0"
|
||||
@ -3392,7 +3393,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/guzzle/issues",
|
||||
"source": "https://github.com/guzzle/guzzle/tree/7.10.5"
|
||||
"source": "https://github.com/guzzle/guzzle/tree/7.11.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -3408,24 +3409,25 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-27T11:53:46+00:00"
|
||||
"time": "2026-06-02T12:40:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/promises",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/promises.git",
|
||||
"reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2"
|
||||
"reference": "4360e982f87f5f258bf872d094647791db2f4c8e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2",
|
||||
"reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e",
|
||||
"reference": "4360e982f87f5f258bf872d094647791db2f4c8e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0"
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
@ -3475,7 +3477,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/promises/issues",
|
||||
"source": "https://github.com/guzzle/promises/tree/2.4.1"
|
||||
"source": "https://github.com/guzzle/promises/tree/2.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -3491,27 +3493,29 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-20T22:57:30+00:00"
|
||||
"time": "2026-06-02T12:23:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
"version": "2.10.3",
|
||||
"version": "2.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/psr7.git",
|
||||
"reference": "7c1472269227dc6f18930bd903d7a88fe6c52130"
|
||||
"reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/7c1472269227dc6f18930bd903d7a88fe6c52130",
|
||||
"reference": "7c1472269227dc6f18930bd903d7a88fe6c52130",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f",
|
||||
"reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/http-message": "^1.1 || ^2.0",
|
||||
"ralouphie/getallheaders": "^3.0"
|
||||
"ralouphie/getallheaders": "^3.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-factory-implementation": "1.0",
|
||||
@ -3592,7 +3596,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/psr7/issues",
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.10.3"
|
||||
"source": "https://github.com/guzzle/psr7/tree/2.11.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -3608,7 +3612,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-27T11:48:20+00:00"
|
||||
"time": "2026-06-02T12:30:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/uri-template",
|
||||
@ -4351,16 +4355,16 @@
|
||||
},
|
||||
{
|
||||
"name": "kirschbaum-development/eloquent-power-joins",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
|
||||
"reference": "3f77b096c1e8b5aa1fc40d7080e55e795f3430ae"
|
||||
"reference": "33c189bd51a510c1ceba67222395ead08a29863a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/3f77b096c1e8b5aa1fc40d7080e55e795f3430ae",
|
||||
"reference": "3f77b096c1e8b5aa1fc40d7080e55e795f3430ae",
|
||||
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/33c189bd51a510c1ceba67222395ead08a29863a",
|
||||
"reference": "33c189bd51a510c1ceba67222395ead08a29863a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -4408,9 +4412,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
|
||||
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.3.1"
|
||||
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.3.2"
|
||||
},
|
||||
"time": "2026-03-29T12:05:03+00:00"
|
||||
"time": "2026-05-28T20:35:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "kyslik/column-sortable",
|
||||
@ -4620,16 +4624,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v13.12.0",
|
||||
"version": "v13.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "6ac27a7fcfa728250c9f77921cb8fb955546b591"
|
||||
"reference": "1daa6d3b4defe46976ccfa4fb0a7ab62717712a2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/6ac27a7fcfa728250c9f77921cb8fb955546b591",
|
||||
"reference": "6ac27a7fcfa728250c9f77921cb8fb955546b591",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/1daa6d3b4defe46976ccfa4fb0a7ab62717712a2",
|
||||
"reference": "1daa6d3b4defe46976ccfa4fb0a7ab62717712a2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -4840,7 +4844,7 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2026-05-26T23:39:26+00:00"
|
||||
"time": "2026-06-02T14:28:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/helpers",
|
||||
@ -6388,16 +6392,16 @@
|
||||
},
|
||||
{
|
||||
"name": "livewire/livewire",
|
||||
"version": "v4.3.0",
|
||||
"version": "v4.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/livewire/livewire.git",
|
||||
"reference": "19ebb1ee4d057debceccf70ff01950e6a6114edc"
|
||||
"reference": "6a9dd03f45a4b200abfd0ff644745b23fa7baaaa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/19ebb1ee4d057debceccf70ff01950e6a6114edc",
|
||||
"reference": "19ebb1ee4d057debceccf70ff01950e6a6114edc",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/6a9dd03f45a4b200abfd0ff644745b23fa7baaaa",
|
||||
"reference": "6a9dd03f45a4b200abfd0ff644745b23fa7baaaa",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -6452,7 +6456,7 @@
|
||||
"description": "A front-end framework for Laravel.",
|
||||
"support": {
|
||||
"issues": "https://github.com/livewire/livewire/issues",
|
||||
"source": "https://github.com/livewire/livewire/tree/v4.3.0"
|
||||
"source": "https://github.com/livewire/livewire/tree/v4.3.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -6460,7 +6464,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-01T00:46:07+00:00"
|
||||
"time": "2026-06-02T08:58:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mailersend/laravel-driver",
|
||||
@ -11012,16 +11016,16 @@
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-backup",
|
||||
"version": "10.2.1",
|
||||
"version": "10.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-backup.git",
|
||||
"reference": "e69bda927005e4909e67b7b86eb7697ed1fe51bc"
|
||||
"reference": "fd8ae12e6a8401dd4de6d3beb5f37d9a627064f3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-backup/zipball/e69bda927005e4909e67b7b86eb7697ed1fe51bc",
|
||||
"reference": "e69bda927005e4909e67b7b86eb7697ed1fe51bc",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-backup/zipball/fd8ae12e6a8401dd4de6d3beb5f37d9a627064f3",
|
||||
"reference": "fd8ae12e6a8401dd4de6d3beb5f37d9a627064f3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -11096,7 +11100,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-backup/issues",
|
||||
"source": "https://github.com/spatie/laravel-backup/tree/10.2.1"
|
||||
"source": "https://github.com/spatie/laravel-backup/tree/10.2.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -11108,7 +11112,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-24T10:30:33+00:00"
|
||||
"time": "2026-06-01T22:44:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-data",
|
||||
@ -11349,16 +11353,16 @@
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-permission",
|
||||
"version": "7.4.1",
|
||||
"version": "7.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/laravel-permission.git",
|
||||
"reference": "ef42ecb781e5534d368a3853fa161e420ad51397"
|
||||
"reference": "15a9daf02ba02d3ae77aaa6da582708231ef999b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/ef42ecb781e5534d368a3853fa161e420ad51397",
|
||||
"reference": "ef42ecb781e5534d368a3853fa161e420ad51397",
|
||||
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/15a9daf02ba02d3ae77aaa6da582708231ef999b",
|
||||
"reference": "15a9daf02ba02d3ae77aaa6da582708231ef999b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -11424,7 +11428,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-permission/issues",
|
||||
"source": "https://github.com/spatie/laravel-permission/tree/7.4.1"
|
||||
"source": "https://github.com/spatie/laravel-permission/tree/7.4.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -11432,7 +11436,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-29T07:59:45+00:00"
|
||||
"time": "2026-05-30T19:21:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-signal-aware-command",
|
||||
@ -12652,79 +12656,6 @@
|
||||
],
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/flex",
|
||||
"version": "v2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/flex.git",
|
||||
"reference": "9cd384775973eabbf6e8b05784dda279fc67c28d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/flex/zipball/9cd384775973eabbf6e8b05784dda279fc67c28d",
|
||||
"reference": "9cd384775973eabbf6e8b05784dda279fc67c28d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^2.1",
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"conflict": {
|
||||
"composer/semver": "<1.7.2",
|
||||
"symfony/dotenv": "<5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^2.1",
|
||||
"symfony/dotenv": "^6.4|^7.4|^8.0",
|
||||
"symfony/filesystem": "^6.4|^7.4|^8.0",
|
||||
"symfony/phpunit-bridge": "^6.4|^7.4|^8.0",
|
||||
"symfony/process": "^6.4|^7.4|^8.0"
|
||||
},
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
"class": "Symfony\\Flex\\Flex"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Flex\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien.potencier@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Composer plugin for Symfony",
|
||||
"support": {
|
||||
"issues": "https://github.com/symfony/flex/issues",
|
||||
"source": "https://github.com/symfony/flex/tree/v2.10.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-16T09:38:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/html-sanitizer",
|
||||
"version": "v7.4.13",
|
||||
@ -16846,6 +16777,65 @@
|
||||
],
|
||||
"time": "2026-03-29T15:46:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carthage-software/mago",
|
||||
"version": "1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/carthage-software/mago.git",
|
||||
"reference": "8aab53f6d004f9a6e85128c365d3d28737fdb272"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/carthage-software/mago/zipball/8aab53f6d004f9a6e85128c365d3d28737fdb272",
|
||||
"reference": "8aab53f6d004f9a6e85128c365d3d28737fdb272",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~8.1 || ~8.2 || ~8.3 || ~8.4 || ~8.5 || ~8.6"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": "To show binary download progress"
|
||||
},
|
||||
"bin": [
|
||||
"composer/bin/mago"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"composer/src/functions.php",
|
||||
"composer/src/internal.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Mago\\": "composer/src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT OR Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Saif Eddin Gmati",
|
||||
"email": "azjezz@carthage.software"
|
||||
}
|
||||
],
|
||||
"description": "Mago is a toolchain for PHP that aims to provide a set of tools to help developers write better code.",
|
||||
"keywords": [
|
||||
"dev"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/carthage-software/mago/issues",
|
||||
"source": "https://github.com/carthage-software/mago/tree/1.29.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/azjezz",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-23T18:53:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "driftingly/rector-laravel",
|
||||
"version": "2.4.0",
|
||||
@ -17348,16 +17338,16 @@
|
||||
},
|
||||
{
|
||||
"name": "larastan/larastan",
|
||||
"version": "v3.9.6",
|
||||
"version": "v3.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/larastan/larastan.git",
|
||||
"reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636"
|
||||
"reference": "2970f83398154178a739609c244577267c7ee8eb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/larastan/larastan/zipball/9ad17e83e96b63536cb6ac39c3d40d29ff9cf636",
|
||||
"reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636",
|
||||
"url": "https://api.github.com/repos/larastan/larastan/zipball/2970f83398154178a739609c244577267c7ee8eb",
|
||||
"reference": "2970f83398154178a739609c244577267c7ee8eb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -17371,17 +17361,17 @@
|
||||
"illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13",
|
||||
"illuminate/support": "^11.44.2 || ^12.4.1 || ^13",
|
||||
"php": "^8.2",
|
||||
"phpstan/phpstan": "^2.1.44"
|
||||
"phpstan/phpstan": "^2.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^13",
|
||||
"doctrine/coding-standard": "^14",
|
||||
"laravel/framework": "^11.44.2 || ^12.7.2 || ^13",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nikic/php-parser": "^5.4",
|
||||
"orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11",
|
||||
"orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.1",
|
||||
"phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8"
|
||||
"phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8 || ^13.1.8"
|
||||
},
|
||||
"suggest": {
|
||||
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
|
||||
@ -17426,7 +17416,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/larastan/larastan/issues",
|
||||
"source": "https://github.com/larastan/larastan/tree/v3.9.6"
|
||||
"source": "https://github.com/larastan/larastan/tree/v3.10.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -17434,7 +17424,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-16T10:02:43+00:00"
|
||||
"time": "2026-05-28T08:00:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/boost",
|
||||
@ -18234,16 +18224,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pestphp/pest",
|
||||
"version": "v4.7.0",
|
||||
"version": "v4.7.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pestphp/pest.git",
|
||||
"reference": "2fc75cfcf03c041c804778fa894282234adc3c66"
|
||||
"reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pestphp/pest/zipball/2fc75cfcf03c041c804778fa894282234adc3c66",
|
||||
"reference": "2fc75cfcf03c041c804778fa894282234adc3c66",
|
||||
"url": "https://api.github.com/repos/pestphp/pest/zipball/40b88b62ef8a7c6fcae5fc28f1fa747f601c131b",
|
||||
"reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -18256,21 +18246,21 @@
|
||||
"pestphp/pest-plugin-mutate": "^4.0.1",
|
||||
"pestphp/pest-plugin-profanity": "^4.2.1",
|
||||
"php": "^8.3.0",
|
||||
"phpunit/phpunit": "^12.5.24",
|
||||
"symfony/process": "^7.4.8|^8.0.8"
|
||||
"phpunit/phpunit": "^12.5.28",
|
||||
"symfony/process": "^7.4.13|^8.1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"filp/whoops": "<2.18.3",
|
||||
"phpunit/phpunit": ">12.5.24",
|
||||
"phpunit/phpunit": ">12.5.28",
|
||||
"sebastian/exporter": "<7.0.0",
|
||||
"webmozart/assert": "<1.11.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mrpunyapal/peststan": "^0.2.9",
|
||||
"mrpunyapal/peststan": "^0.2.10",
|
||||
"pestphp/pest-dev-tools": "^4.1.0",
|
||||
"pestphp/pest-plugin-browser": "^4.3.1",
|
||||
"pestphp/pest-plugin-type-coverage": "^4.0.4",
|
||||
"psy/psysh": "^0.12.22"
|
||||
"psy/psysh": "^0.12.23"
|
||||
},
|
||||
"bin": [
|
||||
"bin/pest"
|
||||
@ -18337,7 +18327,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/pestphp/pest/issues",
|
||||
"source": "https://github.com/pestphp/pest/tree/v4.7.0"
|
||||
"source": "https://github.com/pestphp/pest/tree/v4.7.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -18349,7 +18339,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-03T16:09:32+00:00"
|
||||
"time": "2026-06-01T06:08:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pestphp/pest-plugin",
|
||||
@ -19120,11 +19110,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.56",
|
||||
"version": "2.2.1",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/93a603c9fc3be8c3c93bbc8d22170ad766685537",
|
||||
"reference": "93a603c9fc3be8c3c93bbc8d22170ad766685537",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660",
|
||||
"reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -19147,6 +19137,17 @@
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ondřej Mirtes"
|
||||
},
|
||||
{
|
||||
"name": "Markus Staab"
|
||||
},
|
||||
{
|
||||
"name": "Vincent Langlet"
|
||||
}
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
@ -19169,20 +19170,20 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-26T17:04:57+00:00"
|
||||
"time": "2026-05-28T14:44:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "12.5.6",
|
||||
"version": "12.5.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "876099a072646c7745f673d7aeab5382c4439691"
|
||||
"reference": "186dab580576598076de6818596d12b61801880e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691",
|
||||
"reference": "876099a072646c7745f673d7aeab5382c4439691",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e",
|
||||
"reference": "186dab580576598076de6818596d12b61801880e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -19193,13 +19194,13 @@
|
||||
"php": ">=8.3",
|
||||
"phpunit/php-text-template": "^5.0",
|
||||
"sebastian/complexity": "^5.0",
|
||||
"sebastian/environment": "^8.0.3",
|
||||
"sebastian/lines-of-code": "^4.0",
|
||||
"sebastian/environment": "^8.1.2",
|
||||
"sebastian/lines-of-code": "^4.0.1",
|
||||
"sebastian/version": "^6.0",
|
||||
"theseer/tokenizer": "^2.0.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.5.1"
|
||||
"phpunit/phpunit": "^12.5.28"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcov": "PHP extension that provides line coverage",
|
||||
@ -19237,7 +19238,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -19257,7 +19258,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-15T08:23:17+00:00"
|
||||
"time": "2026-06-01T13:24:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
@ -19518,16 +19519,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.24",
|
||||
"version": "12.5.28",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046"
|
||||
"reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046",
|
||||
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5895d05f5bf421ed230fbd76e1277e4b8955def4",
|
||||
"reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -19546,15 +19547,15 @@
|
||||
"phpunit/php-invoker": "^6.0.0",
|
||||
"phpunit/php-text-template": "^5.0.0",
|
||||
"phpunit/php-timer": "^8.0.0",
|
||||
"sebastian/cli-parser": "^4.2.0",
|
||||
"sebastian/comparator": "^7.1.6",
|
||||
"sebastian/cli-parser": "^4.2.1",
|
||||
"sebastian/comparator": "^7.1.8",
|
||||
"sebastian/diff": "^7.0.0",
|
||||
"sebastian/environment": "^8.1.0",
|
||||
"sebastian/exporter": "^7.0.2",
|
||||
"sebastian/environment": "^8.1.2",
|
||||
"sebastian/exporter": "^7.0.3",
|
||||
"sebastian/global-state": "^8.0.2",
|
||||
"sebastian/object-enumerator": "^7.0.0",
|
||||
"sebastian/recursion-context": "^7.0.1",
|
||||
"sebastian/type": "^6.0.3",
|
||||
"sebastian/type": "^6.0.4",
|
||||
"sebastian/version": "^6.0.0",
|
||||
"staabm/side-effects-detector": "^1.0.5"
|
||||
},
|
||||
@ -19596,7 +19597,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.28"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -19604,7 +19605,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-01T04:21:04+00:00"
|
||||
"time": "2026-05-27T14:01:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psy/psysh",
|
||||
@ -20199,26 +20200,26 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/global-state",
|
||||
"version": "8.0.2",
|
||||
"version": "8.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/global-state.git",
|
||||
"reference": "ef1377171613d09edd25b7816f05be8313f9115d"
|
||||
"reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d",
|
||||
"reference": "ef1377171613d09edd25b7816f05be8313f9115d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9",
|
||||
"reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"sebastian/object-reflector": "^5.0",
|
||||
"sebastian/recursion-context": "^7.0"
|
||||
"sebastian/recursion-context": "^7.0.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-dom": "*",
|
||||
"phpunit/phpunit": "^12.0"
|
||||
"phpunit/phpunit": "^12.5.28"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@ -20249,7 +20250,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/global-state/issues",
|
||||
"security": "https://github.com/sebastianbergmann/global-state/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2"
|
||||
"source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -20269,7 +20270,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-29T11:29:25+00:00"
|
||||
"time": "2026-06-01T15:10:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/lines-of-code",
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @noinspection PhpIllegalPsrClassPathInspection */
|
||||
|
||||
namespace Database\Factories;
|
||||
@ -29,9 +31,9 @@ class AcarsFactory extends Factory
|
||||
'id' => null,
|
||||
'pirep_id' => null,
|
||||
'log' => fake()->text(100),
|
||||
'lat' => fake()->latitude,
|
||||
'lon' => fake()->longitude,
|
||||
'distance' => fake()->randomFloat(2, 0, 6000),
|
||||
'lat' => fake()->latitude(),
|
||||
'lon' => fake()->longitude(),
|
||||
'distance' => fake()->numberBetween(0, 6000),
|
||||
'heading' => fake()->numberBetween(0, 359),
|
||||
'altitude_agl' => fake()->numberBetween(20, 400),
|
||||
'altitude_msl' => fake()->numberBetween(20, 400),
|
||||
|
||||
@ -38,11 +38,10 @@ class AircraftFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'subfleet_id' => fn () => Subfleet::factory()->create()->id,
|
||||
'airport_id' => fn () => Airport::factory()->create()->id,
|
||||
'iata' => fake()->unique()->text(5),
|
||||
'icao' => fake()->unique()->text(5),
|
||||
'iata' => fake()->unique()->lexify('???'),
|
||||
'icao' => fake()->unique()->lexify('????'),
|
||||
'name' => fake()->text(50),
|
||||
'registration' => fake()->unique()->text(10),
|
||||
'hex_code' => ICAO::createHexCode(),
|
||||
|
||||
@ -13,22 +13,11 @@ use Hashids\Hashids;
|
||||
*/
|
||||
class AirlineFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Airline::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'icao' => function (array $apt): string {
|
||||
$hashids = new Hashids(microtime(), 5);
|
||||
$mt = str_replace('.', '', microtime(true));
|
||||
@ -36,8 +25,8 @@ class AirlineFactory extends Factory
|
||||
return $hashids->encode($mt);
|
||||
},
|
||||
'iata' => fn (array $apt) => $apt['icao'],
|
||||
'name' => fake()->sentence(3),
|
||||
'country' => fake()->country,
|
||||
'name' => fake()->company(),
|
||||
'country' => fake()->countryCode(),
|
||||
'active' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @noinspection PhpIllegalPsrClassPathInspection */
|
||||
|
||||
namespace Database\Factories;
|
||||
@ -27,8 +29,7 @@ class AwardFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'name' => fake()->name,
|
||||
'name' => fake()->name(),
|
||||
'description' => fake()->text(10),
|
||||
'ref_model_type' => null,
|
||||
'ref_model_params' => null,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Event;
|
||||
|
||||
@ -30,10 +30,9 @@ class ExpenseFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'airline_id' => null,
|
||||
'name' => fake()->text(20),
|
||||
'amount' => fake()->randomFloat(2, 100, 1000),
|
||||
'amount' => fake()->numberBetween(100, 1000),
|
||||
'type' => ExpenseType::FLIGHT,
|
||||
'multiplier' => false,
|
||||
'ref_model_type' => Expense::class,
|
||||
|
||||
@ -30,7 +30,6 @@ class FareFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'code' => fake()->unique()->text(50),
|
||||
'name' => fake()->text(50),
|
||||
'price' => fake()->randomFloat(2, 100, 1000),
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @noinspection PhpIllegalPsrClassPathInspection */
|
||||
|
||||
namespace Database\Factories;
|
||||
@ -28,10 +30,9 @@ class NewsFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'user_id' => fn () => User::factory()->create()->id,
|
||||
'subject' => fake()->text(),
|
||||
'body' => fake()->sentence,
|
||||
'subject' => fake()->sentence(),
|
||||
'body' => fake()->text(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ class PirepFactory extends Factory
|
||||
'planned_distance' => fake()->randomFloat(2, 0, 6000),
|
||||
'flight_time' => fake()->numberBetween(60, 360),
|
||||
'planned_flight_time' => fake()->numberBetween(60, 360),
|
||||
'zfw' => fake()->randomFloat(2),
|
||||
'zfw' => fake()->randomFloat(2, 0, 500000),
|
||||
'block_fuel' => fake()->randomFloat(2, 0, 1000),
|
||||
'fuel_used' => fn (array $pirep): float => round($pirep['block_fuel'] * .9, 2),
|
||||
'block_on_time' => Carbon::now('UTC'),
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @noinspection PhpIllegalPsrClassPathInspection */
|
||||
|
||||
namespace Database\Factories;
|
||||
@ -27,7 +29,6 @@ class RankFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'name' => fake()->unique()->text(50),
|
||||
'hours' => fake()->numberBetween(10, 50),
|
||||
'acars_base_pay_rate' => fake()->numberBetween(10, 100),
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @noinspection PhpIllegalPsrClassPathInspection */
|
||||
|
||||
namespace Database\Factories;
|
||||
@ -27,8 +29,7 @@ class RoleFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'name' => fake()->name,
|
||||
'name' => fake()->name(),
|
||||
'guard_name' => 'web',
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @noinspection PhpIllegalPsrClassPathInspection */
|
||||
|
||||
namespace Database\Factories;
|
||||
@ -28,7 +30,6 @@ class SubfleetFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'id' => null,
|
||||
'airline_id' => fn () => Airline::factory()->create()->id,
|
||||
'name' => fake()->unique()->text(50),
|
||||
'type' => fake()->unique()->text(7),
|
||||
|
||||
@ -37,7 +37,6 @@ class UserFactory extends Factory
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => null,
|
||||
'pilot_id' => null,
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->safeEmail(),
|
||||
|
||||
@ -12,7 +12,7 @@ return new class() extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('migrations_data')) {
|
||||
Schema::create('migrations_data', function (Blueprint $table) {
|
||||
Schema::create('migrations_data', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -23,7 +23,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('users')) {
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -67,7 +67,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('acars')) {
|
||||
Schema::create('acars', function (Blueprint $table) {
|
||||
Schema::create('acars', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -100,7 +100,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('activity_log')) {
|
||||
Schema::create('activity_log', function (Blueprint $table) {
|
||||
Schema::create('activity_log', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -122,7 +122,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('aircraft')) {
|
||||
Schema::create('aircraft', function (Blueprint $table) {
|
||||
Schema::create('aircraft', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -145,7 +145,7 @@ return new class() extends Migration
|
||||
$table->string('simbrief_type', 25)->nullable();
|
||||
$table->decimal('fuel_onboard')->unsigned()->nullable()->default(0);
|
||||
$table->unsignedBigInteger('flight_time')->nullable()->default(0);
|
||||
$table->char('status', 1)->default('A');
|
||||
$table->string('status', 1)->default('A');
|
||||
$table->unsignedTinyInteger('state')->default(0);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
@ -153,7 +153,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('airlines')) {
|
||||
Schema::create('airlines', function (Blueprint $table) {
|
||||
Schema::create('airlines', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -175,7 +175,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('airports')) {
|
||||
Schema::create('airports', function (Blueprint $table) {
|
||||
Schema::create('airports', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -201,7 +201,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('awards')) {
|
||||
Schema::create('awards', function (Blueprint $table) {
|
||||
Schema::create('awards', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -218,7 +218,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('bids')) {
|
||||
Schema::create('bids', function (Blueprint $table) {
|
||||
Schema::create('bids', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -233,7 +233,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('events')) {
|
||||
Schema::create('events', function (Blueprint $table) {
|
||||
Schema::create('events', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -249,7 +249,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('expenses')) {
|
||||
Schema::create('expenses', function (Blueprint $table) {
|
||||
Schema::create('expenses', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -257,7 +257,7 @@ return new class() extends Migration
|
||||
$table->unsignedInteger('airline_id')->nullable();
|
||||
$table->string('name');
|
||||
$table->unsignedInteger('amount');
|
||||
$table->char('type');
|
||||
$table->string('type', 1);
|
||||
$table->string('flight_type', 50)->nullable();
|
||||
$table->boolean('charge_to_user')->nullable()->default(false);
|
||||
$table->boolean('multiplier')->nullable()->default(false);
|
||||
@ -271,7 +271,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('failed_jobs')) {
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
Schema::create('failed_jobs', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -285,7 +285,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('fares')) {
|
||||
Schema::create('fares', function (Blueprint $table) {
|
||||
Schema::create('fares', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -304,7 +304,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('files')) {
|
||||
Schema::create('files', function (Blueprint $table) {
|
||||
Schema::create('files', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -324,7 +324,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('flight_fare')) {
|
||||
Schema::create('flight_fare', function (Blueprint $table) {
|
||||
Schema::create('flight_fare', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -340,7 +340,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('flight_field_values')) {
|
||||
Schema::create('flight_field_values', function (Blueprint $table) {
|
||||
Schema::create('flight_field_values', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -354,7 +354,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('flight_fields')) {
|
||||
Schema::create('flight_fields', function (Blueprint $table) {
|
||||
Schema::create('flight_fields', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -365,7 +365,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('flight_subfleet')) {
|
||||
Schema::create('flight_subfleet', function (Blueprint $table) {
|
||||
Schema::create('flight_subfleet', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -379,7 +379,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('flights')) {
|
||||
Schema::create('flights', function (Blueprint $table) {
|
||||
Schema::create('flights', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -397,7 +397,7 @@ return new class() extends Migration
|
||||
$table->unsignedInteger('level')->nullable()->default(0);
|
||||
$table->decimal('distance')->unsigned()->nullable()->default(0);
|
||||
$table->unsignedInteger('flight_time')->nullable();
|
||||
$table->char('flight_type', 1)->default('J');
|
||||
$table->string('flight_type', 1)->default('J');
|
||||
$table->decimal('load_factor', 5)->nullable();
|
||||
$table->decimal('load_factor_variance', 5)->nullable();
|
||||
$table->text('route')->nullable();
|
||||
@ -422,7 +422,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('invites')) {
|
||||
Schema::create('invites', function (Blueprint $table) {
|
||||
Schema::create('invites', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -437,7 +437,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('jobs')) {
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
Schema::create('jobs', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -452,7 +452,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('journal_transactions')) {
|
||||
Schema::create('journal_transactions', function (Blueprint $table) {
|
||||
Schema::create('journal_transactions', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -475,7 +475,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('journals')) {
|
||||
Schema::create('journals', function (Blueprint $table) {
|
||||
Schema::create('journals', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -493,7 +493,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('kvp')) {
|
||||
Schema::create('kvp', function (Blueprint $table) {
|
||||
Schema::create('kvp', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -503,7 +503,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('ledgers')) {
|
||||
Schema::create('ledgers', function (Blueprint $table) {
|
||||
Schema::create('ledgers', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -515,7 +515,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('navdata')) {
|
||||
Schema::create('navdata', function (Blueprint $table) {
|
||||
Schema::create('navdata', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -531,7 +531,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('news')) {
|
||||
Schema::create('news', function (Blueprint $table) {
|
||||
Schema::create('news', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -544,7 +544,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('notifications')) {
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
Schema::create('notifications', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -552,7 +552,7 @@ return new class() extends Migration
|
||||
$table->string('type');
|
||||
$table->string('notifiable_type');
|
||||
$table->unsignedBigInteger('notifiable_id');
|
||||
$table->text('data');
|
||||
$table->json('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
@ -561,7 +561,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('pages')) {
|
||||
Schema::create('pages', function (Blueprint $table) {
|
||||
Schema::create('pages', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -580,7 +580,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('password_resets')) {
|
||||
Schema::create('password_resets', function (Blueprint $table) {
|
||||
Schema::create('password_resets', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -633,7 +633,7 @@ return new class() extends Migration
|
||||
}*/
|
||||
|
||||
if (!Schema::hasTable('pirep_comments')) {
|
||||
Schema::create('pirep_comments', function (Blueprint $table) {
|
||||
Schema::create('pirep_comments', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -646,7 +646,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('pirep_fares')) {
|
||||
Schema::create('pirep_fares', function (Blueprint $table) {
|
||||
Schema::create('pirep_fares', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -665,7 +665,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('pirep_field_values')) {
|
||||
Schema::create('pirep_field_values', function (Blueprint $table) {
|
||||
Schema::create('pirep_field_values', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -680,7 +680,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('pirep_fields')) {
|
||||
Schema::create('pirep_fields', function (Blueprint $table) {
|
||||
Schema::create('pirep_fields', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -694,7 +694,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('pireps')) {
|
||||
Schema::create('pireps', function (Blueprint $table) {
|
||||
Schema::create('pireps', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -707,7 +707,7 @@ return new class() extends Migration
|
||||
$table->string('flight_number', 10)->nullable()->index();
|
||||
$table->string('route_code', 5)->nullable();
|
||||
$table->string('route_leg', 5)->nullable();
|
||||
$table->char('flight_type', 1)->default('J');
|
||||
$table->string('flight_type', 1)->default('J');
|
||||
$table->string('dpt_airport_id', 5)->index();
|
||||
$table->string('arr_airport_id', 5)->index();
|
||||
$table->string('alt_airport_id', 5)->nullable();
|
||||
@ -726,7 +726,7 @@ return new class() extends Migration
|
||||
$table->unsignedTinyInteger('source')->nullable()->default(0);
|
||||
$table->string('source_name', 50)->nullable();
|
||||
$table->unsignedSmallInteger('state')->default(1);
|
||||
$table->char('status', 3)->default('SCH');
|
||||
$table->string('status', 3)->default('SCH');
|
||||
$table->dateTime('submitted_at')->nullable();
|
||||
$table->dateTime('block_off_time')->nullable();
|
||||
$table->dateTime('block_on_time')->nullable();
|
||||
@ -736,7 +736,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('ranks')) {
|
||||
Schema::create('ranks', function (Blueprint $table) {
|
||||
Schema::create('ranks', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -786,7 +786,7 @@ return new class() extends Migration
|
||||
}*/
|
||||
|
||||
if (!Schema::hasTable('sessions')) {
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
Schema::create('sessions', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -800,7 +800,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('settings')) {
|
||||
Schema::create('settings', function (Blueprint $table) {
|
||||
Schema::create('settings', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -820,7 +820,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('simbrief')) {
|
||||
Schema::create('simbrief', function (Blueprint $table) {
|
||||
Schema::create('simbrief', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -840,7 +840,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('simbrief_aircraft')) {
|
||||
Schema::create('simbrief_aircraft', function (Blueprint $table) {
|
||||
Schema::create('simbrief_aircraft', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -853,7 +853,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('simbrief_airframes')) {
|
||||
Schema::create('simbrief_airframes', function (Blueprint $table) {
|
||||
Schema::create('simbrief_airframes', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -869,7 +869,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('simbrief_layouts')) {
|
||||
Schema::create('simbrief_layouts', function (Blueprint $table) {
|
||||
Schema::create('simbrief_layouts', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -881,7 +881,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('stats')) {
|
||||
Schema::create('stats', function (Blueprint $table) {
|
||||
Schema::create('stats', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -895,7 +895,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('subfleet_fare')) {
|
||||
Schema::create('subfleet_fare', function (Blueprint $table) {
|
||||
Schema::create('subfleet_fare', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -912,7 +912,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('subfleet_rank')) {
|
||||
Schema::create('subfleet_rank', function (Blueprint $table) {
|
||||
Schema::create('subfleet_rank', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -927,7 +927,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('subfleets')) {
|
||||
Schema::create('subfleets', function (Blueprint $table) {
|
||||
Schema::create('subfleets', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -950,7 +950,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('typerating_subfleet')) {
|
||||
Schema::create('typerating_subfleet', function (Blueprint $table) {
|
||||
Schema::create('typerating_subfleet', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -963,7 +963,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('typerating_user')) {
|
||||
Schema::create('typerating_user', function (Blueprint $table) {
|
||||
Schema::create('typerating_user', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -976,7 +976,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('typeratings')) {
|
||||
Schema::create('typeratings', function (Blueprint $table) {
|
||||
Schema::create('typeratings', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -993,7 +993,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('user_awards')) {
|
||||
Schema::create('user_awards', function (Blueprint $table) {
|
||||
Schema::create('user_awards', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -1007,7 +1007,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('user_field_values')) {
|
||||
Schema::create('user_field_values', function (Blueprint $table) {
|
||||
Schema::create('user_field_values', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -1022,7 +1022,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('user_fields')) {
|
||||
Schema::create('user_fields', function (Blueprint $table) {
|
||||
Schema::create('user_fields', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -1039,7 +1039,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('user_oauth_tokens')) {
|
||||
Schema::create('user_oauth_tokens', function (Blueprint $table) {
|
||||
Schema::create('user_oauth_tokens', function (Blueprint $table): void {
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
$table->charset = 'utf8mb4';
|
||||
|
||||
@ -1054,7 +1054,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (Schema::hasTable('permission_role')) {
|
||||
Schema::table('permission_role', function (Blueprint $table) {
|
||||
Schema::table('permission_role', function (Blueprint $table): void {
|
||||
// Check if the foreign key already exists
|
||||
// See https://github.com/laravel/framework/discussions/43443
|
||||
$foreignKeys = collect(Schema::getForeignKeys('permission_role'));
|
||||
@ -1074,7 +1074,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (Schema::hasTable('permission_user')) {
|
||||
Schema::table('permission_user', function (Blueprint $table) {
|
||||
Schema::table('permission_user', function (Blueprint $table): void {
|
||||
$foreignKeys = collect(Schema::getForeignKeys('permission_user'));
|
||||
|
||||
if ($foreignKeys->where('name', 'permission_user_permission_id_foreign')->count() === 0) {
|
||||
@ -1086,7 +1086,7 @@ return new class() extends Migration
|
||||
}
|
||||
|
||||
if (Schema::hasTable('role_user')) {
|
||||
Schema::table('role_user', function (Blueprint $table) {
|
||||
Schema::table('role_user', function (Blueprint $table): void {
|
||||
$foreignKeys = collect(Schema::getForeignKeys('role_user'));
|
||||
|
||||
if ($foreignKeys->where('name', 'role_user_role_id_foreign')->count() === 0) {
|
||||
|
||||
@ -10,7 +10,8 @@ return new class() extends Migration
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{// 1. Convert existing comma-separated strings to JSON arrays
|
||||
{
|
||||
// 1. Convert existing comma-separated strings to JSON arrays
|
||||
DB::table('expenses')->orderBy('id')->chunk(100, function ($expenses): void {
|
||||
foreach ($expenses as $expense) {
|
||||
// Only process if it has a value and isn't already a JSON array
|
||||
@ -33,8 +34,13 @@ return new class() extends Migration
|
||||
|
||||
// 2. Officially change the column type to JSON
|
||||
// Note: You must have doctrine/dbal installed for this step on older Laravel versions
|
||||
Schema::table('expenses', function (Blueprint $table): void {
|
||||
$table->json('flight_type')->nullable()->change();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() === 'pgsql') {
|
||||
DB::statement('ALTER TABLE expenses ALTER COLUMN flight_type TYPE JSON USING flight_type::json');
|
||||
} else {
|
||||
Schema::table('expenses', function (Blueprint $table): void {
|
||||
$table->json('flight_type')->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -42,7 +42,12 @@ return new class() extends Migration
|
||||
|
||||
// Phase 1: canonicalize route_code / route_leg storage.
|
||||
DB::table('flights')->whereIn('route_code', ['', '0'])->update(['route_code' => null]);
|
||||
DB::table('flights')->whereIn('route_leg', ['', '0', 0])->update(['route_leg' => null]);
|
||||
DB::table('flights')->whereIn('route_leg', ['0', 0])->update(['route_leg' => null]);
|
||||
|
||||
// SQLite's type affinity allows '' in integer columns; other drivers reject it.
|
||||
if ($driver === 'sqlite') {
|
||||
DB::table('flights')->where('route_leg', '')->update(['route_leg' => null]);
|
||||
}
|
||||
|
||||
// Phase 2: auto-disable pre-existing duplicates.
|
||||
//
|
||||
@ -59,8 +64,8 @@ return new class() extends Migration
|
||||
// matching the strict-duplicate key semantics.
|
||||
$rankedSubquery = DB::table('flights')
|
||||
->select('id')
|
||||
->selectRaw("FIRST_VALUE(id) OVER (PARTITION BY bundle_id, airline_id, flight_number, COALESCE(route_code, ''), COALESCE(route_leg, '') ORDER BY id) as kept_id")
|
||||
->selectRaw("ROW_NUMBER() OVER (PARTITION BY bundle_id, airline_id, flight_number, COALESCE(route_code, ''), COALESCE(route_leg, '') ORDER BY id) as rn")
|
||||
->selectRaw("FIRST_VALUE(id) OVER (PARTITION BY bundle_id, airline_id, flight_number, COALESCE(route_code, ''), COALESCE(route_leg, 0) ORDER BY id) as kept_id")
|
||||
->selectRaw("ROW_NUMBER() OVER (PARTITION BY bundle_id, airline_id, flight_number, COALESCE(route_code, ''), COALESCE(route_leg, 0) ORDER BY id) as rn")
|
||||
->where('enabled', true)
|
||||
->whereNull('owner_type');
|
||||
|
||||
@ -162,8 +167,12 @@ return new class() extends Migration
|
||||
'mysql', 'mariadb' => 'CASE WHEN enabled = 1 AND owner_type IS NULL '
|
||||
."THEN CONCAT_WS('|', bundle_id, airline_id, flight_number, COALESCE(route_code, ''), COALESCE(route_leg, '')) "
|
||||
.'ELSE NULL END',
|
||||
// PostgreSQL marks CONCAT_WS as STABLE (not IMMUTABLE) because of
|
||||
// implicit type coercions, which disqualifies it from STORED
|
||||
// generated columns. The `||` operator over text is IMMUTABLE, so
|
||||
// we cast non-text columns explicitly and COALESCE nullable ones.
|
||||
'pgsql' => 'CASE WHEN enabled = TRUE AND owner_type IS NULL '
|
||||
."THEN CONCAT_WS('|', bundle_id, airline_id, flight_number, COALESCE(route_code, ''), COALESCE(route_leg::text, '')) "
|
||||
."THEN bundle_id::text || '|' || airline_id::text || '|' || flight_number::text || '|' || COALESCE(route_code::text, '') || '|' || COALESCE(route_leg::text, '') "
|
||||
.'ELSE NULL END',
|
||||
'sqlite' => 'CASE WHEN enabled = 1 AND owner_type IS NULL '
|
||||
."THEN (bundle_id || '|' || airline_id || '|' || flight_number || '|' || COALESCE(route_code, '') || '|' || COALESCE(route_leg, '')) "
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('airlines', function (Blueprint $table): void {
|
||||
$table->string('icao', 12)->change();
|
||||
$table->string('iata', 12)->nullable()->change();
|
||||
$table->string('country', 3)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('activity_log', function (Blueprint $table): void {
|
||||
$table->string('subject_id', 128)->nullable()->change();
|
||||
$table->string('batch_uuid', 36)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('aircraft', function (Blueprint $table): void {
|
||||
$table->string('icao', 12)->nullable()->change();
|
||||
$table->string('iata', 12)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('pireps', function (Blueprint $table): void {
|
||||
$table->decimal('zfw', 12, 2)->unsigned()->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('news', function (Blueprint $table): void {
|
||||
$table->string('subject', 200)->change();
|
||||
});
|
||||
|
||||
Schema::table('flights', function (Blueprint $table): void {
|
||||
$table->string('callsign', 10)->nullable()->change();
|
||||
});
|
||||
|
||||
if (DB::getDriverName() === 'pgsql') {
|
||||
// `increments()->change()` does NOT create a sequence/identity on PG —
|
||||
// the change path of the grammar skips the auto-increment modifiers.
|
||||
// Attach an identity explicitly and advance it past existing rows.
|
||||
DB::statement('ALTER TABLE events ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY');
|
||||
DB::statement("SELECT setval(pg_get_serial_sequence('events', 'id'), COALESCE((SELECT MAX(id) FROM events), 0) + 1, false)");
|
||||
} else {
|
||||
Schema::table('events', function (Blueprint $table): void {
|
||||
$table->increments('id')->change();
|
||||
});
|
||||
}
|
||||
|
||||
if (DB::getDriverName() === 'pgsql') {
|
||||
DB::statement('ALTER TABLE user_field_values ALTER COLUMN user_id TYPE bigint USING user_id::bigint');
|
||||
} else {
|
||||
Schema::table('user_field_values', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('user_id')->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::getDriverName() === 'pgsql') {
|
||||
DB::statement('ALTER TABLE user_field_values ALTER COLUMN user_id TYPE varchar(16) USING user_id::varchar');
|
||||
} else {
|
||||
Schema::table('user_field_values', function (Blueprint $table): void {
|
||||
$table->string('user_id', 16)->change();
|
||||
});
|
||||
}
|
||||
|
||||
if (DB::getDriverName() === 'pgsql') {
|
||||
DB::statement('ALTER TABLE events ALTER COLUMN id DROP IDENTITY IF EXISTS');
|
||||
} else {
|
||||
Schema::table('events', function (Blueprint $table): void {
|
||||
$table->integer('id')->change();
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('flights', function (Blueprint $table): void {
|
||||
$table->string('callsign', 4)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('news', function (Blueprint $table): void {
|
||||
$table->string('subject', 191)->change();
|
||||
});
|
||||
|
||||
Schema::table('pireps', function (Blueprint $table): void {
|
||||
$table->decimal('zfw', 8, 2)->unsigned()->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('aircraft', function (Blueprint $table): void {
|
||||
$table->string('icao', 4)->nullable()->change();
|
||||
$table->string('iata', 4)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('activity_log', function (Blueprint $table): void {
|
||||
$table->char('subject_id', 36)->nullable()->change();
|
||||
$table->char('batch_uuid', 36)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('airlines', function (Blueprint $table): void {
|
||||
$table->string('icao', 5)->change();
|
||||
$table->string('iata', 5)->nullable()->change();
|
||||
$table->string('country', 2)->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$isPgsql = DB::getDriverName() === 'pgsql';
|
||||
|
||||
if ($isPgsql) {
|
||||
DB::statement('ALTER TABLE expenses ALTER COLUMN type TYPE varchar(1) USING type::varchar(1)');
|
||||
DB::statement('ALTER TABLE aircraft ALTER COLUMN status TYPE varchar(1) USING status::varchar(1)');
|
||||
DB::statement('ALTER TABLE flights ALTER COLUMN flight_type TYPE varchar(1) USING flight_type::varchar(1)');
|
||||
DB::statement('ALTER TABLE pireps ALTER COLUMN flight_type TYPE varchar(1) USING flight_type::varchar(1)');
|
||||
DB::statement('ALTER TABLE pireps ALTER COLUMN status TYPE varchar(3) USING status::varchar(3)');
|
||||
|
||||
// Re-assert column defaults explicitly so they survive the type change
|
||||
// regardless of PostgreSQL version behavior.
|
||||
DB::statement("ALTER TABLE aircraft ALTER COLUMN status SET DEFAULT 'A'");
|
||||
DB::statement("ALTER TABLE flights ALTER COLUMN flight_type SET DEFAULT 'J'");
|
||||
DB::statement("ALTER TABLE pireps ALTER COLUMN flight_type SET DEFAULT 'J'");
|
||||
DB::statement("ALTER TABLE pireps ALTER COLUMN status SET DEFAULT 'SCH'");
|
||||
} else {
|
||||
Schema::table('expenses', function (Blueprint $table): void {
|
||||
$table->string('type', 1)->change();
|
||||
});
|
||||
|
||||
Schema::table('aircraft', function (Blueprint $table): void {
|
||||
$table->string('status', 1)->default('A')->change();
|
||||
});
|
||||
|
||||
Schema::table('flights', function (Blueprint $table): void {
|
||||
$table->string('flight_type', 1)->default('J')->change();
|
||||
});
|
||||
|
||||
Schema::table('pireps', function (Blueprint $table): void {
|
||||
$table->string('flight_type', 1)->default('J')->change();
|
||||
$table->string('status', 3)->default('SCH')->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$isPgsql = DB::getDriverName() === 'pgsql';
|
||||
|
||||
if ($isPgsql) {
|
||||
DB::statement('ALTER TABLE pireps ALTER COLUMN status TYPE character(3) USING status::character(3)');
|
||||
DB::statement('ALTER TABLE pireps ALTER COLUMN flight_type TYPE character(1) USING flight_type::character(1)');
|
||||
DB::statement('ALTER TABLE flights ALTER COLUMN flight_type TYPE character(1) USING flight_type::character(1)');
|
||||
DB::statement('ALTER TABLE aircraft ALTER COLUMN status TYPE character(1) USING status::character(1)');
|
||||
DB::statement('ALTER TABLE expenses ALTER COLUMN type TYPE character(1) USING type::character(1)');
|
||||
|
||||
DB::statement("ALTER TABLE aircraft ALTER COLUMN status SET DEFAULT 'A'");
|
||||
DB::statement("ALTER TABLE flights ALTER COLUMN flight_type SET DEFAULT 'J'");
|
||||
DB::statement("ALTER TABLE pireps ALTER COLUMN flight_type SET DEFAULT 'J'");
|
||||
DB::statement("ALTER TABLE pireps ALTER COLUMN status SET DEFAULT 'SCH'");
|
||||
} else {
|
||||
Schema::table('pireps', function (Blueprint $table): void {
|
||||
$table->char('status', 3)->default('SCH')->change();
|
||||
$table->char('flight_type', 1)->default('J')->change();
|
||||
});
|
||||
|
||||
Schema::table('flights', function (Blueprint $table): void {
|
||||
$table->char('flight_type', 1)->default('J')->change();
|
||||
});
|
||||
|
||||
Schema::table('aircraft', function (Blueprint $table): void {
|
||||
$table->char('status', 1)->default('A')->change();
|
||||
});
|
||||
|
||||
Schema::table('expenses', function (Blueprint $table): void {
|
||||
$table->char('type')->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class() extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
// 1. Normalize existing values so the cast to JSON can't fail.
|
||||
// Null/blank or non-decodable payloads become an empty JSON object.
|
||||
DB::table('notifications')->orderBy('id')->chunk(100, function ($notifications): void {
|
||||
foreach ($notifications as $notification) {
|
||||
$value = $notification->data;
|
||||
|
||||
if (blank($value) || json_decode((string) $value) === null) {
|
||||
DB::table('notifications')
|
||||
->where('id', $notification->id)
|
||||
->update(['data' => '{}']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Officially change the column type to JSON.
|
||||
if ($driver === 'pgsql') {
|
||||
DB::statement('ALTER TABLE notifications ALTER COLUMN data TYPE json USING data::json');
|
||||
} elseif ($driver === 'mysql') {
|
||||
DB::statement('ALTER TABLE notifications MODIFY COLUMN data JSON NOT NULL');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
if ($driver === 'pgsql') {
|
||||
DB::statement('ALTER TABLE notifications ALTER COLUMN data TYPE text USING data::text');
|
||||
} elseif ($driver === 'mysql') {
|
||||
DB::statement('ALTER TABLE notifications MODIFY COLUMN data TEXT NOT NULL');
|
||||
}
|
||||
}
|
||||
};
|
||||
61
database/seeders/BaseDataSeeder.php
Normal file
61
database/seeders/BaseDataSeeder.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Kvp;
|
||||
use App\Models\Rank;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class BaseDataSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$this->createInitialRanks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific key has been seeded
|
||||
*/
|
||||
private function isSeeded(string $key): bool
|
||||
{
|
||||
$key .= '_seeded';
|
||||
|
||||
return Kvp::where('key', $key)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a specific key as seeded
|
||||
*/
|
||||
private function setSeeded(string $key): void
|
||||
{
|
||||
$key .= '_seeded';
|
||||
Kvp::updateOrCreate(['key' => $key], ['value' => Carbon::now('UTC')->toDateTimeString()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create initial ranks if they don't exist
|
||||
*/
|
||||
private function createInitialRanks(): void
|
||||
{
|
||||
// Seems like they added ranks, so we don't need to do anything
|
||||
// This is mainly a check for updates
|
||||
if ($this->isSeeded('ranks') || Rank::count() > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rank::firstOrCreate(
|
||||
['id' => 1],
|
||||
[
|
||||
'name' => 'New Pilot',
|
||||
'hours' => 0,
|
||||
'acars_base_pay_rate' => 50,
|
||||
'manual_base_pay_rate' => 25,
|
||||
],
|
||||
);
|
||||
|
||||
$this->setSeeded('ranks');
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,8 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$seeders = [
|
||||
ShieldSeeder::class,
|
||||
YamlSeeder::class,
|
||||
SettingsSeeder::class,
|
||||
BaseDataSeeder::class,
|
||||
];
|
||||
|
||||
// Always insert the samples in the demo environment
|
||||
|
||||
857
database/seeders/SettingsSeeder.php
Normal file
857
database/seeders/SettingsSeeder.php
Normal file
@ -0,0 +1,857 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SettingsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Default setting definitions. On re-seed, `value` is never overwritten
|
||||
* (only inserted for new rows) so user changes are preserved.
|
||||
*
|
||||
* @var list<array{key: string, name: string, value: string, group: string, type: string, options: string, description: string}>
|
||||
*/
|
||||
private array $settings = [
|
||||
// General
|
||||
[
|
||||
'key' => 'general.theme',
|
||||
'name' => 'Current Theme',
|
||||
'group' => 'general',
|
||||
'value' => 'seven',
|
||||
'type' => 'select',
|
||||
'options' => '',
|
||||
'description' => 'The currently active theme',
|
||||
],
|
||||
[
|
||||
'key' => 'general.start_date',
|
||||
'name' => 'Start Date',
|
||||
'group' => 'general',
|
||||
'value' => '',
|
||||
'type' => 'date',
|
||||
'options' => '',
|
||||
'description' => 'The date your VA started',
|
||||
],
|
||||
[
|
||||
'key' => 'general.admin_email',
|
||||
'name' => 'Admin Email',
|
||||
'group' => 'general',
|
||||
'value' => '',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'Email where system notices, etc are sent',
|
||||
],
|
||||
[
|
||||
'key' => 'general.auto_airport_lookup',
|
||||
'name' => 'Automatic airport lookup',
|
||||
'group' => 'general',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => "If an airport isn't added, try to look it up when adding schedules",
|
||||
],
|
||||
[
|
||||
'key' => 'general.allow_unadded_airports',
|
||||
'name' => 'Allow unadded airports',
|
||||
'group' => 'general',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'If an un-added airport is used, it is looked up and added',
|
||||
],
|
||||
[
|
||||
'key' => 'general.check_prerelease_version',
|
||||
'name' => 'Pre-release versions in version check',
|
||||
'group' => 'general',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Include beta and other pre-release versions when checking for a new version',
|
||||
],
|
||||
[
|
||||
'key' => 'general.telemetry',
|
||||
'name' => 'Send telemetry to phpVMS',
|
||||
'group' => 'general',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Send some data (php version, mysql version) to phpVMS. See AnalyticsSvc code for details',
|
||||
],
|
||||
[
|
||||
'key' => 'general.google_analytics_id',
|
||||
'name' => 'Google Analytics Tracking ID',
|
||||
'group' => 'general',
|
||||
'value' => '',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'Enter your Google Analytics Tracking ID',
|
||||
],
|
||||
[
|
||||
'key' => 'general.record_user_ip',
|
||||
'name' => 'Record user IP address',
|
||||
'group' => 'general',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => "Record the user's IP address on register/login",
|
||||
],
|
||||
[
|
||||
'key' => 'general.invite_only_registrations',
|
||||
'name' => 'Invite Only Registrations',
|
||||
'group' => 'general',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'If checked, only users with an invite can register',
|
||||
],
|
||||
[
|
||||
'key' => 'general.disable_registrations',
|
||||
'name' => 'Disable registrations',
|
||||
'group' => 'general',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'If checked, registrations will be disabled and only admins can add pilots',
|
||||
],
|
||||
[
|
||||
'key' => 'general.auto_language_detection',
|
||||
'name' => 'Auto language detection',
|
||||
'group' => 'general',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => "If checked, the app's language will be inferred from the browser's preferences",
|
||||
],
|
||||
|
||||
// Captcha
|
||||
[
|
||||
'key' => 'captcha.enabled',
|
||||
'name' => 'hCaptcha Enabled',
|
||||
'group' => 'captcha',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Is hCaptcha enabled',
|
||||
],
|
||||
[
|
||||
'key' => 'captcha.site_key',
|
||||
'name' => 'hCaptcha Site Key',
|
||||
'group' => 'captcha',
|
||||
'value' => '',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'Your hCaptcha Site Key',
|
||||
],
|
||||
[
|
||||
'key' => 'captcha.secret_key',
|
||||
'name' => 'hCaptcha Secret Key',
|
||||
'group' => 'captcha',
|
||||
'value' => '',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'Your hCaptcha Secret Key',
|
||||
],
|
||||
|
||||
// Units
|
||||
[
|
||||
'key' => 'units.currency',
|
||||
'name' => 'Currency',
|
||||
'group' => 'units',
|
||||
'value' => 'USD',
|
||||
'type' => 'select',
|
||||
'options' => '',
|
||||
'description' => 'The currency to use',
|
||||
],
|
||||
[
|
||||
'key' => 'units.distance',
|
||||
'name' => 'Distance Units',
|
||||
'group' => 'units',
|
||||
'value' => 'nmi',
|
||||
'type' => 'select',
|
||||
'options' => 'km=kilometers,mi=miles,nmi=nautical miles',
|
||||
'description' => 'The distance unit for display',
|
||||
],
|
||||
[
|
||||
'key' => 'units.weight',
|
||||
'name' => 'Weight Units',
|
||||
'group' => 'units',
|
||||
'value' => 'lbs',
|
||||
'type' => 'select',
|
||||
'options' => 'lbs,kg',
|
||||
'description' => 'The weight unit for display',
|
||||
],
|
||||
[
|
||||
'key' => 'units.speed',
|
||||
'name' => 'Speed Units',
|
||||
'group' => 'units',
|
||||
'value' => 'knot',
|
||||
'type' => 'select',
|
||||
'options' => 'km/h,knot',
|
||||
'description' => 'The speed unit for display',
|
||||
],
|
||||
[
|
||||
'key' => 'units.altitude',
|
||||
'name' => 'Altitude Units',
|
||||
'group' => 'units',
|
||||
'value' => 'ft',
|
||||
'type' => 'select',
|
||||
'options' => 'ft=feet,m=meters',
|
||||
'description' => 'The altitude unit for display',
|
||||
],
|
||||
[
|
||||
'key' => 'units.fuel',
|
||||
'name' => 'Fuel Units',
|
||||
'group' => 'units',
|
||||
'value' => 'lbs',
|
||||
'type' => 'select',
|
||||
'options' => 'lbs,kg',
|
||||
'description' => 'The units for fuel for display',
|
||||
],
|
||||
[
|
||||
'key' => 'units.volume',
|
||||
'name' => 'Volume Units',
|
||||
'group' => 'units',
|
||||
'value' => 'gallons',
|
||||
'type' => 'select',
|
||||
'options' => 'gallons,l=liters',
|
||||
'description' => 'The units of volume for display',
|
||||
],
|
||||
[
|
||||
'key' => 'units.temperature',
|
||||
'name' => 'Temperature Units',
|
||||
'group' => 'units',
|
||||
'value' => 'F',
|
||||
'type' => 'select',
|
||||
'options' => 'F=Fahrenheit,C=Celsius',
|
||||
'description' => 'The units for temperature',
|
||||
],
|
||||
|
||||
// ACARS
|
||||
[
|
||||
'key' => 'acars.live_time',
|
||||
'name' => 'Live Time',
|
||||
'group' => 'acars',
|
||||
'value' => '12',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'Age of flights to show on the map in hours. Set to 0 to show only all in-progress flights',
|
||||
],
|
||||
[
|
||||
'key' => 'acars.center_coords',
|
||||
'name' => 'Center Coords',
|
||||
'group' => 'acars',
|
||||
'value' => '30.1945,-97.6699',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'Where to center the map; enter as LAT,LON',
|
||||
],
|
||||
[
|
||||
'key' => 'acars.default_zoom',
|
||||
'name' => 'Default Zoom',
|
||||
'group' => 'acars',
|
||||
'value' => '5',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'Initial zoom level on the map',
|
||||
],
|
||||
[
|
||||
'key' => 'acars.update_interval',
|
||||
'name' => 'Refresh Interval',
|
||||
'group' => 'acars',
|
||||
'value' => '60',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'How often the live map updates its data',
|
||||
],
|
||||
|
||||
// Airports
|
||||
[
|
||||
'key' => 'airports.default_ground_handling_cost',
|
||||
'name' => 'Default Ground Handling Cost',
|
||||
'group' => 'airports',
|
||||
'value' => '250',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => "If an airport's Ground Handling Cost isn't added, set this value by default",
|
||||
],
|
||||
[
|
||||
'key' => 'airports.default_jet_a_fuel_cost',
|
||||
'name' => 'Default Jet A Fuel Cost',
|
||||
'group' => 'airports',
|
||||
'value' => '0.7',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => "If an airport's Jet A Fuel Cost isn't added, set this value by default",
|
||||
],
|
||||
[
|
||||
'key' => 'airports.default_100ll_fuel_cost',
|
||||
'name' => 'Default 100LL Fuel Cost',
|
||||
'group' => 'airports',
|
||||
'value' => '0.9',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => "If an airport's 100LL Fuel Cost isn't added, set this value by default",
|
||||
],
|
||||
[
|
||||
'key' => 'airports.default_mogas_fuel_cost',
|
||||
'name' => 'Default MOGAS Fuel Cost',
|
||||
'group' => 'airports',
|
||||
'value' => '0.8',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => "If an airport's MOGAS Fuel Cost isn't added, set this value by default",
|
||||
],
|
||||
|
||||
// Bids
|
||||
[
|
||||
'key' => 'bids.disable_flight_on_bid',
|
||||
'name' => 'Disable flight on bid',
|
||||
'group' => 'bids',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'When a flight is bid on, no one else can bid on it',
|
||||
],
|
||||
[
|
||||
'key' => 'bids.allow_multiple_bids',
|
||||
'name' => 'Allow multiple bids',
|
||||
'group' => 'bids',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Whether or not someone can bid on multiple flights',
|
||||
],
|
||||
[
|
||||
'key' => 'bids.block_aircraft',
|
||||
'name' => 'Restrict Aircraft',
|
||||
'group' => 'bids',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'When enabled, an aircraft can only be used for one active Bid and Flight/Pirep',
|
||||
],
|
||||
[
|
||||
'key' => 'bids.expire_time',
|
||||
'name' => 'Expire Time',
|
||||
'group' => 'bids',
|
||||
'value' => '48',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'Number of hours to expire bids after',
|
||||
],
|
||||
|
||||
// Flights
|
||||
[
|
||||
'key' => 'flights.default_load_factor',
|
||||
'name' => 'Load Factor',
|
||||
'group' => 'flights',
|
||||
'value' => '82',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'The default load factor for a flight, as a percent',
|
||||
],
|
||||
[
|
||||
'key' => 'flights.load_factor_variance',
|
||||
'name' => 'Load Factor Variance',
|
||||
'group' => 'flights',
|
||||
'value' => '5',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'How much the load factor can vary per-flight',
|
||||
],
|
||||
[
|
||||
'key' => 'flights.use_cargo_load_factor',
|
||||
'name' => 'Different Cargo Load Factor',
|
||||
'group' => 'flights',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'When enabled these values will be used for cargo fares',
|
||||
],
|
||||
[
|
||||
'key' => 'flights.default_cargo_load_factor',
|
||||
'name' => 'Cargo Load Factor',
|
||||
'group' => 'flights',
|
||||
'value' => '32',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'The default cargo load factor for a flight, as a percent',
|
||||
],
|
||||
[
|
||||
'key' => 'flights.cargo_load_factor_variance',
|
||||
'name' => 'Cargo Load Factor Variance',
|
||||
'group' => 'flights',
|
||||
'value' => '5',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'How much the cargo load factor can vary per-flight',
|
||||
],
|
||||
[
|
||||
'key' => 'flights.only_company_aircraft',
|
||||
'name' => 'Allow Only Company Aircraft',
|
||||
'group' => 'flights',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'If no subfleets are assigned to a flight, only company aircraft will be used',
|
||||
],
|
||||
|
||||
// SimBrief
|
||||
[
|
||||
'key' => 'simbrief.api_key',
|
||||
'name' => 'Simbrief API Key',
|
||||
'group' => 'simbrief',
|
||||
'value' => '',
|
||||
'type' => 'string',
|
||||
'options' => '',
|
||||
'description' => 'Your Simbrief API key',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.only_bids',
|
||||
'name' => 'Only allow for bids',
|
||||
'group' => 'simbrief',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Only allow briefs to be created for bidded flights',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.expire_hours',
|
||||
'name' => 'Simbrief Expire Time',
|
||||
'group' => 'simbrief',
|
||||
'value' => '6',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'Hours after how long to remove unused briefs',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.noncharter_pax_weight',
|
||||
'name' => 'Non-Charter Passenger Weight',
|
||||
'group' => 'simbrief',
|
||||
'value' => '185',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'Passenger weight for non-charter flights excluding baggage (lbs)',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.noncharter_baggage_weight',
|
||||
'name' => 'Non-Charter Baggage Weight',
|
||||
'group' => 'simbrief',
|
||||
'value' => '35',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'Passenger baggage weight for non-charter flights (lbs)',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.charter_pax_weight',
|
||||
'name' => 'Charter Passenger Weight',
|
||||
'group' => 'simbrief',
|
||||
'value' => '168',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'Passenger weight for charter flights excluding baggage (lbs)',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.charter_baggage_weight',
|
||||
'name' => 'Charter Baggage Weight',
|
||||
'group' => 'simbrief',
|
||||
'value' => '28',
|
||||
'type' => 'number',
|
||||
'options' => '',
|
||||
'description' => 'Passenger baggage weight for charter flights (lbs)',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.callsign',
|
||||
'name' => 'Use ATC Callsign',
|
||||
'group' => 'simbrief',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Use pilot ident as Simbrief ATC Callsign',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.name_private',
|
||||
'name' => 'Use Privatized Name at OFPs',
|
||||
'group' => 'simbrief',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Use privatized user name as SimBrief OFP captain name',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.block_aircraft',
|
||||
'name' => 'Restrict Aircraft',
|
||||
'group' => 'simbrief',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'When enabled, an aircraft can only be used for one active SimBrief OFP and Flight/Pirep',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.use_standard_weights',
|
||||
'name' => 'Use Only phpVMS Weights',
|
||||
'group' => 'simbrief',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'When enabled, only phpVMS Passenger and Baggage weights will be used (instead of Airframe definitions)',
|
||||
],
|
||||
[
|
||||
'key' => 'simbrief.use_custom_airframes',
|
||||
'name' => 'Use Only Custom Airframes',
|
||||
'group' => 'simbrief',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'When enabled, only phpVMS Airframes will be listed for flight planning (instead of combined list)',
|
||||
],
|
||||
|
||||
// PIREPs
|
||||
[
|
||||
'key' => 'pireps.duplicate_check_time',
|
||||
'name' => 'PIREP duplicate time check',
|
||||
'group' => 'pireps',
|
||||
'value' => '10',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'The time in minutes to check for a duplicate PIREP',
|
||||
],
|
||||
[
|
||||
'key' => 'pireps.restrict_aircraft_to_rank',
|
||||
'name' => 'Restrict Aircraft to Ranks',
|
||||
'group' => 'pireps',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => "Aircraft restricted to user's rank",
|
||||
],
|
||||
[
|
||||
'key' => 'pireps.restrict_aircraft_to_typerating',
|
||||
'name' => 'Restrict Aircraft by Type Ratings',
|
||||
'group' => 'pireps',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Aircraft restricted to user type ratings',
|
||||
],
|
||||
[
|
||||
'key' => 'pireps.only_aircraft_at_dpt_airport',
|
||||
'name' => 'Restrict Aircraft At Departure',
|
||||
'group' => 'pireps',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Only allow aircraft that are at the departure airport',
|
||||
],
|
||||
[
|
||||
'key' => 'pireps.advanced_fuel',
|
||||
'name' => 'Advanced Fuel Calculations',
|
||||
'group' => 'pireps',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Enables remaining fuel amounts to be considered for fuel expenses',
|
||||
],
|
||||
[
|
||||
'key' => 'pireps.delete_cancelled_hours',
|
||||
'name' => 'Delete cancelled PIREPs',
|
||||
'group' => 'pireps',
|
||||
'value' => '12',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'The time in hours to delete a cancelled PIREP',
|
||||
],
|
||||
[
|
||||
'key' => 'pireps.delete_rejected_hours',
|
||||
'name' => 'Delete rejected PIREPs',
|
||||
'group' => 'pireps',
|
||||
'value' => '12',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'The time in hours to delete a rejected PIREP',
|
||||
],
|
||||
[
|
||||
'key' => 'pireps.handle_diversion',
|
||||
'name' => 'Handle pirep diversion',
|
||||
'group' => 'pireps',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Auto Handle Diversions (move assets and create re position flight)',
|
||||
],
|
||||
|
||||
// Pilots
|
||||
[
|
||||
'key' => 'pilots.id_length',
|
||||
'name' => 'Pilot ID Length',
|
||||
'group' => 'pilots',
|
||||
'value' => '4',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => "The length of a pilot's ID",
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.id_code',
|
||||
'name' => 'Pilot ID Code',
|
||||
'group' => 'pilots',
|
||||
'value' => '',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'Fixed ICAO code for pilot IDs',
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.auto_accept',
|
||||
'name' => 'Auto Accept New Pilot',
|
||||
'group' => 'pilots',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Automatically accept a pilot when they register',
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.home_hubs_only',
|
||||
'name' => 'Hubs as home airport',
|
||||
'group' => 'pilots',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Pilots can only select hubs as their home airport',
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.only_flights_from_current',
|
||||
'name' => 'Only allow flights from Current',
|
||||
'group' => 'pilots',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Only allow flights from their current location',
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.only_show_flights_from_current',
|
||||
'name' => 'Only show flights from Current',
|
||||
'group' => 'pilots',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Only show flights from their current location',
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.auto_leave_days',
|
||||
'name' => 'Pilot to ON LEAVE days',
|
||||
'group' => 'pilots',
|
||||
'value' => '30',
|
||||
'type' => 'int',
|
||||
'options' => '',
|
||||
'description' => 'Automatically set a pilot to ON LEAVE status after N days of no activity',
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.hide_inactive',
|
||||
'name' => 'Hide Inactive Pilots',
|
||||
'group' => 'pilots',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => "Don't show inactive pilots in the public view",
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.restrict_to_company',
|
||||
'name' => 'Restrict the flights to company',
|
||||
'group' => 'pilots',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => "Restrict flights to the user's airline",
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.allow_transfer_hours',
|
||||
'name' => 'Allow transfer hours',
|
||||
'group' => 'pilots',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Allow specifying transfer hours on registration page and displayed on profile page',
|
||||
],
|
||||
[
|
||||
'key' => 'pilots.count_transfer_hours',
|
||||
'name' => 'Count transfer hours in calculations',
|
||||
'group' => 'pilots',
|
||||
'value' => 'false',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Count transfer hours in calculations, like ranks and the total hours',
|
||||
],
|
||||
|
||||
// Notifications
|
||||
[
|
||||
'key' => 'notifications.discord_public_webhook_url',
|
||||
'name' => 'Discord Public Webhook URL',
|
||||
'group' => 'notifications',
|
||||
'value' => '',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'The Discord Webhook URL for public notifications',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.discord_private_webhook_url',
|
||||
'name' => 'Discord Private Webhook URL',
|
||||
'group' => 'notifications',
|
||||
'value' => '',
|
||||
'type' => 'text',
|
||||
'options' => '',
|
||||
'description' => 'The Discord Webhook URL for private notifications',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.discord_pirep_status',
|
||||
'name' => 'Discord Pirep Messages (Public)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Pirep status messages (Only key events are being sent)',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.mail_pirep_admin',
|
||||
'name' => 'Pirep Filed (Admin)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Pirep filed mails sent to admins',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.mail_pirep_user_ack',
|
||||
'name' => 'Pirep Accepted (Pilot)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Pirep Accepted mails sent to pilots',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.mail_pirep_user_rej',
|
||||
'name' => 'Pirep Rejected (Pilot)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Pirep Rejected mails sent to pilots',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.mail_news',
|
||||
'name' => 'News Mails',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'News mails sent to all members',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.discord_award_awarded',
|
||||
'name' => 'Discord Award Message (Public)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Send out a discord notification when a user is awarded',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.discord_user_rank_changed',
|
||||
'name' => 'Discord User Rank Changed (Public)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => "Send out a discord notification when a user's rank is changed",
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.discord_pirep_diverted',
|
||||
'name' => 'Discord Pirep Diverted (Public)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Send out a discord notification when a pirep is diverted',
|
||||
],
|
||||
[
|
||||
'key' => 'notifications.discord_pirep_filed',
|
||||
'name' => 'Discord Pirep Filed Message (Public)',
|
||||
'group' => 'notifications',
|
||||
'value' => 'true',
|
||||
'type' => 'boolean',
|
||||
'options' => 'true,false',
|
||||
'description' => 'Send out a discord notification when a pirep is filed',
|
||||
],
|
||||
|
||||
// Cron
|
||||
[
|
||||
'key' => 'cron.random_id',
|
||||
'name' => 'Cron Randomized ID',
|
||||
'group' => 'cron',
|
||||
'value' => '',
|
||||
'type' => 'hidden',
|
||||
'options' => '',
|
||||
'description' => '',
|
||||
],
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$groupOffsets = [];
|
||||
$groupOrders = [];
|
||||
$rows = [];
|
||||
|
||||
foreach ($this->settings as $setting) {
|
||||
$group = $setting['group'];
|
||||
|
||||
if (!isset($groupOffsets[$group])) {
|
||||
$groupOffsets[$group] = 0;
|
||||
$groupOrders[$group] = 0;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'id' => Setting::formatKey($setting['key']),
|
||||
'key' => $setting['key'],
|
||||
'name' => $setting['name'],
|
||||
'value' => $setting['value'],
|
||||
'default' => $setting['value'],
|
||||
'group' => $group,
|
||||
'offset' => $groupOffsets[$group],
|
||||
'order' => $groupOrders[$group],
|
||||
'type' => $setting['type'],
|
||||
'options' => $setting['options'],
|
||||
'description' => $setting['description'],
|
||||
];
|
||||
|
||||
$groupOffsets[$group]++;
|
||||
$groupOrders[$group]++;
|
||||
}
|
||||
|
||||
Setting::upsert(
|
||||
$rows,
|
||||
uniqueBy: ['id'],
|
||||
update: ['key', 'name', 'group', 'offset', 'order', 'type', 'options', 'description', 'default'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any settings defined in this seeder are missing from the database.
|
||||
*/
|
||||
public function settingsPending(): bool
|
||||
{
|
||||
foreach ($this->settings as $setting) {
|
||||
$id = Setting::formatKey($setting['key']);
|
||||
|
||||
if (Setting::where('id', $id)->doesntExist()) {
|
||||
Log::info('Setting '.$setting['name'].' missing, update available');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -4,20 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Services\Installer\SeederService;
|
||||
use Carbon\Carbon;
|
||||
use App\Services\YamlDatabaseService;
|
||||
use Exception;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
use function in_array;
|
||||
|
||||
class YamlSeeder extends Seeder
|
||||
{
|
||||
@ -27,10 +20,6 @@ class YamlSeeder extends Seeder
|
||||
'pireps',
|
||||
];
|
||||
|
||||
/**
|
||||
* Columns whose names end with `_time` that genuinely store datetime/timestamp
|
||||
* values (not integer durations like `flight_time`).
|
||||
*/
|
||||
protected array $datetimeTimeColumns = [
|
||||
'arrival_time',
|
||||
'block_off_time',
|
||||
@ -41,7 +30,7 @@ class YamlSeeder extends Seeder
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly SeederService $seederSvc
|
||||
private readonly YamlDatabaseService $databaseSvc
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -51,11 +40,6 @@ class YamlSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->seedFromYamlFile(database_path('seeders/base/base.yml'));
|
||||
|
||||
// Special method to sync the settings
|
||||
$this->seederSvc->syncAllSettings();
|
||||
|
||||
$env = App::environment();
|
||||
$seedPath = database_path('seeders/'.$env);
|
||||
if (!File::isDirectory($seedPath)) {
|
||||
@ -69,157 +53,7 @@ class YamlSeeder extends Seeder
|
||||
->each(function (SplFileInfo $file): void {
|
||||
$path = $file->getPathname();
|
||||
Log::info('reading '.$path);
|
||||
$this->seedFromYamlFile($path);
|
||||
$this->databaseSvc->seedFromYamlFile($path);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function seedFromYamlFile(string $yaml_file, bool $ignore_errors = false): array
|
||||
{
|
||||
$yml = file_get_contents($yaml_file);
|
||||
if ($yml === false) {
|
||||
throw new \RuntimeException('Unable to read YAML seed file: '.$yaml_file);
|
||||
}
|
||||
|
||||
$yml = Yaml::parse($yml);
|
||||
|
||||
return $this->seedFromYaml($yml, $ignore_errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function seedFromYaml(mixed $yml, bool $ignore_errors = false): array
|
||||
{
|
||||
$imported = [];
|
||||
|
||||
if (empty($yml)) {
|
||||
return $imported;
|
||||
}
|
||||
|
||||
foreach ($yml as $table => $data) {
|
||||
// set the number imported to zero
|
||||
$imported[$table] = 0;
|
||||
|
||||
$id_column = 'id';
|
||||
if (array_key_exists('id_column', $data)) {
|
||||
$id_column = $data['id_column'];
|
||||
}
|
||||
|
||||
$ignore_on_update = [];
|
||||
if (array_key_exists('ignore_on_update', $data)) {
|
||||
$ignore_on_update = $data['ignore_on_update'];
|
||||
}
|
||||
|
||||
$ignore_if_exists = false;
|
||||
if (array_key_exists('ignore_if_exists', $data)) {
|
||||
$ignore_if_exists = $data['ignore_if_exists'];
|
||||
}
|
||||
|
||||
$rows = array_key_exists('data', $data) ? $data['data'] : $data;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
$this->insertRow(
|
||||
$table,
|
||||
$row,
|
||||
$id_column,
|
||||
$ignore_on_update,
|
||||
$ignore_errors,
|
||||
$ignore_if_exists
|
||||
);
|
||||
} catch (QueryException $e) {
|
||||
if ($ignore_errors) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$imported[$table]++;
|
||||
}
|
||||
}
|
||||
|
||||
return $imported;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function insertRow(
|
||||
string $table,
|
||||
array $row = [],
|
||||
string $id_col = 'id',
|
||||
array $ignore_on_updates = [],
|
||||
bool $ignore_errors = true,
|
||||
bool $ignore_if_exists = true,
|
||||
): array {
|
||||
if ($row === []) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
if (!array_key_exists('id', $row) && in_array($table, $this->uuidTables, true)) {
|
||||
$row['id'] = Str::uuid();
|
||||
}
|
||||
|
||||
// encrypt any password fields
|
||||
if (array_key_exists('password', $row)) {
|
||||
$row['password'] = bcrypt($row['password']);
|
||||
}
|
||||
|
||||
// Convert datetime fields. Only process columns whose names indicate
|
||||
// they hold timestamps (ending in _at, or specific _time columns
|
||||
// that are actual datetime types, not integer durations).
|
||||
foreach ($row as $column => $value) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isDateTimeColumn = str_ends_with((string) $column, '_at')
|
||||
|| in_array($column, $this->datetimeTimeColumns, true);
|
||||
if (!$isDateTimeColumn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strtolower((string) $value) === 'now') {
|
||||
$row[$column] = Carbon::now('UTC')->toDateTimeString();
|
||||
} else {
|
||||
$row[$column] = Carbon::parse($value)->toDateTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
if (array_key_exists($id_col, $row)) {
|
||||
$count = DB::table($table)->where($id_col, $row[$id_col])->count($id_col);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($count > 0) {
|
||||
if ($ignore_if_exists) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
foreach ($ignore_on_updates as $ignore_column) {
|
||||
if (array_key_exists($ignore_column, $row)) {
|
||||
unset($row[$ignore_column]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::table($table)
|
||||
->where($id_col, $row[$id_col])
|
||||
->update($row);
|
||||
} else {
|
||||
DB::table($table)->insert($row);
|
||||
}
|
||||
} catch (QueryException $queryException) {
|
||||
Log::error('Error while running query: '.$queryException->getMessage(), ['exception' => $queryException]);
|
||||
if (!$ignore_errors) {
|
||||
throw $queryException;
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
#
|
||||
# Initial minimal data required. You probably don't want to modify or erase any of this here
|
||||
#
|
||||
ranks:
|
||||
id_column: id
|
||||
ignore_if_exists: true
|
||||
data:
|
||||
- id: 1
|
||||
name: "New Pilot"
|
||||
hours: 0
|
||||
acars_base_pay_rate: 50
|
||||
manual_base_pay_rate: 25
|
||||
@ -1,586 +0,0 @@
|
||||
- key: general.theme
|
||||
name: "Current Theme"
|
||||
group: general
|
||||
value: "seven"
|
||||
options: ""
|
||||
type: select
|
||||
description: "The currently active theme"
|
||||
- key: general.start_date
|
||||
name: "Start Date"
|
||||
group: general
|
||||
value: ""
|
||||
options: ""
|
||||
type: date
|
||||
description: "The date your VA started"
|
||||
- key: general.admin_email
|
||||
name: "Admin Email"
|
||||
group: general
|
||||
value: ""
|
||||
options: ""
|
||||
type: text
|
||||
description: "Email where system notices, etc are sent"
|
||||
- key: general.auto_airport_lookup
|
||||
name: "Automatic airport lookup"
|
||||
group: general
|
||||
value: true
|
||||
options:
|
||||
type: boolean
|
||||
description: If an airport isn't added, try to look it up when adding schedules
|
||||
- key: general.allow_unadded_airports
|
||||
name: "Allow unadded airports"
|
||||
group: general
|
||||
value: false
|
||||
options:
|
||||
type: boolean
|
||||
description: If an un-added airport is used, it is looked up and added
|
||||
- key: general.check_prerelease_version
|
||||
name: "Pre-release versions in version check"
|
||||
group: general
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Include beta and other pre-release versions when checking for a new version"
|
||||
- key: general.telemetry
|
||||
name: "Send telemetry to phpVMS"
|
||||
group: general
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Send some data (php version, mysql version) to phpVMS. See AnalyticsSvc code for details"
|
||||
- key: general.google_analytics_id
|
||||
name: "Google Analytics Tracking ID"
|
||||
group: general
|
||||
value: ""
|
||||
options: ""
|
||||
type: text
|
||||
description: "Enter your Google Analytics Tracking ID"
|
||||
- key: general.record_user_ip
|
||||
name: "Record user IP address"
|
||||
group: general
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Record the user's IP address on register/login
|
||||
- key: general.invite_only_registrations
|
||||
name: "Invite Only Registrations"
|
||||
group: general
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: If checked, only users with an invite can register
|
||||
- key: general.disable_registrations
|
||||
name: "Disable registrations"
|
||||
group: general
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: If checked, registrations will be disabled and only admins can add pilots
|
||||
- key: general.auto_language_detection
|
||||
name: "Auto language detection"
|
||||
group: general
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: If checked, the app's language will be inferred from the browser's preferences
|
||||
- key: captcha.enabled
|
||||
name: "hCaptcha Enabled"
|
||||
group: captcha
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Is hCaptcha enabled
|
||||
- key: captcha.site_key
|
||||
name: "hCaptcha Site Key"
|
||||
group: captcha
|
||||
value: ""
|
||||
options: ""
|
||||
type: text
|
||||
description: Your hCaptcha Site Key
|
||||
- key: captcha.secret_key
|
||||
name: "hCaptcha Secret Key"
|
||||
group: captcha
|
||||
value: ""
|
||||
options: ""
|
||||
type: text
|
||||
description: Your hCaptcha Secret Key
|
||||
- key: units.currency
|
||||
name: "Currency"
|
||||
group: units
|
||||
value: USD
|
||||
type: select
|
||||
description: "The currency to use"
|
||||
- key: units.distance
|
||||
name: "Distance Units"
|
||||
group: units
|
||||
value: nmi
|
||||
options: "km=kilometers,mi=miles,nmi=nautical miles"
|
||||
type: select
|
||||
description: "The distance unit for display"
|
||||
- key: units.weight
|
||||
name: "Weight Units"
|
||||
group: units
|
||||
value: lbs
|
||||
options: "lbs,kg"
|
||||
type: select
|
||||
description: "The weight unit for display"
|
||||
- key: units.speed
|
||||
name: "Speed Units"
|
||||
group: units
|
||||
value: knot
|
||||
options: "km/h,knot"
|
||||
type: select
|
||||
description: "The speed unit for display"
|
||||
- key: units.altitude
|
||||
name: "Altitude Units"
|
||||
group: units
|
||||
value: ft
|
||||
options: "ft=feet,m=meters"
|
||||
type: select
|
||||
description: "The altitude unit for display"
|
||||
- key: units.fuel
|
||||
name: "Fuel Units"
|
||||
group: units
|
||||
value: lbs
|
||||
options: "lbs,kg"
|
||||
type: select
|
||||
description: "The units for fuel for display"
|
||||
- key: units.volume
|
||||
name: "Volume Units"
|
||||
group: units
|
||||
value: gallons
|
||||
options: "gallons,l=liters"
|
||||
type: select
|
||||
description: "The units of volume for display"
|
||||
- key: units.temperature
|
||||
name: "Temperature Units"
|
||||
group: units
|
||||
value: F
|
||||
options: "F=Fahrenheit,C=Celsius"
|
||||
type: select
|
||||
description: "The units for temperature"
|
||||
- key: acars.live_time
|
||||
name: "Live Time"
|
||||
group: acars
|
||||
value: 12
|
||||
options: ""
|
||||
type: int
|
||||
description: "Age of flights to show on the map in hours. Set to 0 to show only all in-progress flights"
|
||||
- key: acars.center_coords
|
||||
name: "Center Coords"
|
||||
group: acars
|
||||
value: "30.1945,-97.6699"
|
||||
options: ""
|
||||
type: text
|
||||
description: "Where to center the map; enter as LAT,LON"
|
||||
- key: acars.default_zoom
|
||||
name: "Default Zoom"
|
||||
group: acars
|
||||
value: 5
|
||||
options: ""
|
||||
type: int
|
||||
description: "Initial zoom level on the map"
|
||||
- key: acars.update_interval
|
||||
name: "Refresh Interval"
|
||||
group: acars
|
||||
value: 60
|
||||
options: ""
|
||||
type: int
|
||||
description: "How often the live map updates its data"
|
||||
- key: airports.default_ground_handling_cost
|
||||
name: "Default Ground Handling Cost"
|
||||
group: airports
|
||||
value: 250
|
||||
options:
|
||||
type: int
|
||||
description: If an airport's Ground Handling Cost Cost isn't added, set this value by default
|
||||
- key: airports.default_jet_a_fuel_cost
|
||||
name: "Default Jet A Fuel Cost"
|
||||
group: airports
|
||||
value: 0.7
|
||||
options:
|
||||
type: text
|
||||
description: If an airport's Jet A Fuel Cost isn't added, set this value by default
|
||||
- key: airports.default_100ll_fuel_cost
|
||||
name: "Default 100LL Fuel Cost"
|
||||
group: airports
|
||||
value: 0.9
|
||||
options:
|
||||
type: text
|
||||
description: If an airport's 100LL Fuel Cost isn't added, set this value by default
|
||||
- key: airports.default_mogas_fuel_cost
|
||||
name: "Default MOGAS Fuel Cost"
|
||||
group: airports
|
||||
value: 0.8
|
||||
options:
|
||||
type: text
|
||||
description: If an airport's MOGAS Fuel Cost isn't added, set this value by default
|
||||
- key: bids.disable_flight_on_bid
|
||||
name: "Disable flight on bid"
|
||||
group: bids
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "When a flight is bid on, no one else can bid on it"
|
||||
- key: bids.allow_multiple_bids
|
||||
name: "Allow multiple bids"
|
||||
group: bids
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Whether or not someone can bid on multiple flights"
|
||||
- key: bids.block_aircraft
|
||||
name: "Restrict Aircraft"
|
||||
group: bids
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "When enabled, an aircraft can only be used for one active Bid and Flight/Pirep"
|
||||
- key: bids.expire_time
|
||||
name: "Expire Time"
|
||||
group: bids
|
||||
value: 48
|
||||
options: ""
|
||||
type: int
|
||||
description: "Number of hours to expire bids after"
|
||||
- key: flights.default_load_factor
|
||||
name: "Load Factor"
|
||||
group: flights
|
||||
value: 82
|
||||
options: ""
|
||||
type: number
|
||||
description: "The default load factor for a flight, as a percent"
|
||||
- key: flights.load_factor_variance
|
||||
name: "Load Factor Variance"
|
||||
group: flights
|
||||
value: 5
|
||||
options: ""
|
||||
type: number
|
||||
description: "How much the load factor can vary per-flight"
|
||||
- key: flights.use_cargo_load_factor
|
||||
name: "Different Cargo Load Factor"
|
||||
group: flights
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "When enabled these values will be used for cargo fares"
|
||||
- key: flights.default_cargo_load_factor
|
||||
name: "Cargo Load Factor"
|
||||
group: flights
|
||||
value: 32
|
||||
options: ""
|
||||
type: number
|
||||
description: "The default cargo load factor for a flight, as a percent"
|
||||
- key: flights.cargo_load_factor_variance
|
||||
name: "Cargo Load Factor Variance"
|
||||
group: flights
|
||||
value: 5
|
||||
options: ""
|
||||
type: number
|
||||
description: "How much the cargo load factor can vary per-flight"
|
||||
- key: flights.only_company_aircraft
|
||||
name: "Allow Only Company Aircraft"
|
||||
group: flights
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "If no subfleets are assigned to a flight, only company aircraft will be used"
|
||||
- key: simbrief.api_key
|
||||
name: "Simbrief API Key"
|
||||
group: simbrief
|
||||
value: ""
|
||||
options: ""
|
||||
type: string
|
||||
description: "Your Simbrief API key"
|
||||
- key: simbrief.only_bids
|
||||
name: "Only allow for bids"
|
||||
group: simbrief
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Only allow briefs to be created for bidded flights"
|
||||
- key: simbrief.expire_hours
|
||||
name: "Simbrief Expire Time"
|
||||
group: simbrief
|
||||
value: 6
|
||||
options: ""
|
||||
type: number
|
||||
description: "Hours after how long to remove unused briefs"
|
||||
- key: simbrief.noncharter_pax_weight
|
||||
name: "Non-Charter Passenger Weight"
|
||||
group: simbrief
|
||||
value: 185
|
||||
options: ""
|
||||
type: number
|
||||
description: "Passenger weight for non-charter flights excluding baggage (lbs)"
|
||||
- key: simbrief.noncharter_baggage_weight
|
||||
name: "Non-Charter Baggage Weight"
|
||||
group: simbrief
|
||||
value: 35
|
||||
options: ""
|
||||
type: number
|
||||
description: "Passenger baggage weight for non-charter flights (lbs)"
|
||||
- key: simbrief.charter_pax_weight
|
||||
name: "Charter Passenger Weight"
|
||||
group: simbrief
|
||||
value: 168
|
||||
options: ""
|
||||
type: number
|
||||
description: "Passenger weight for charter flights excluding baggage (lbs)"
|
||||
- key: simbrief.charter_baggage_weight
|
||||
name: "Charter Baggage Weight"
|
||||
group: simbrief
|
||||
value: 28
|
||||
options: ""
|
||||
type: number
|
||||
description: "Passenger baggage weight for charter flights (lbs)"
|
||||
- key: simbrief.callsign
|
||||
name: "Use ATC Callsign"
|
||||
group: simbrief
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Use pilot ident as Simbrief ATC Callsign"
|
||||
- key: simbrief.name_private
|
||||
name: "Use Privatized Name at OFPs"
|
||||
group: simbrief
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Use privatized user name as SimBrief OFP captain name"
|
||||
- key: simbrief.block_aircraft
|
||||
name: "Restrict Aircraft"
|
||||
group: simbrief
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "When enabled, an aircraft can only be used for one active SimBrief OFP and Flight/Pirep"
|
||||
- key: simbrief.use_standard_weights
|
||||
name: "Use Only phpVMS Weights"
|
||||
group: simbrief
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "When enabled, only phpVMS Passenger and Baggage weights will be used (instead of Airframe definitions)"
|
||||
- key: simbrief.use_custom_airframes
|
||||
name: "Use Only Custom Airframes"
|
||||
group: simbrief
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "When enabled, only phpVMS Airframes will be listed for flight planning (instead of combined list)"
|
||||
- key: pireps.duplicate_check_time
|
||||
name: "PIREP duplicate time check"
|
||||
group: pireps
|
||||
value: 10
|
||||
options: ""
|
||||
type: int
|
||||
description: "The time in minutes to check for a duplicate PIREP"
|
||||
- key: pireps.restrict_aircraft_to_rank
|
||||
name: "Restrict Aircraft to Ranks"
|
||||
group: pireps
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Aircraft restricted to user's rank"
|
||||
- key: pireps.restrict_aircraft_to_typerating
|
||||
name: "Restrict Aircraft by Type Ratings"
|
||||
group: pireps
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Aircraft restricted to user type ratings"
|
||||
- key: pireps.only_aircraft_at_dpt_airport
|
||||
name: "Restrict Aircraft At Departure"
|
||||
group: pireps
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Only allow aircraft that are at the departure airport"
|
||||
- key: pireps.advanced_fuel
|
||||
name: "Advanced Fuel Calculations"
|
||||
group: pireps
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Enables remaining fuel amounts to be considered for fuel expenses"
|
||||
- key: pireps.delete_cancelled_hours
|
||||
name: "Delete cancelled PIREPs"
|
||||
group: pireps
|
||||
value: 12
|
||||
options: ""
|
||||
type: int
|
||||
description: "The time in hours to delete a cancelled PIREP"
|
||||
- key: pireps.delete_rejected_hours
|
||||
name: "Delete rejected PIREPs"
|
||||
group: pireps
|
||||
value: 12
|
||||
options: ""
|
||||
type: int
|
||||
description: "The time in hours to delete a rejected PIREP"
|
||||
- key: pireps.handle_diversion
|
||||
name: "Handle pirep diversion"
|
||||
group: pireps
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Auto Handle Diversions (move assets and create re position flight)"
|
||||
- key: pilots.id_length
|
||||
name: "Pilot ID Length"
|
||||
group: pilots
|
||||
value: 4
|
||||
options: ""
|
||||
type: int
|
||||
description: "The length of a pilot's ID"
|
||||
- key: pilots.id_code
|
||||
name: "Pilot ID Code"
|
||||
group: pilots
|
||||
value: ""
|
||||
options: ""
|
||||
type: text
|
||||
description: "Fixed ICAO code for pilot IDs"
|
||||
- key: pilots.auto_accept
|
||||
name: "Auto Accept New Pilot"
|
||||
group: pilots
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Automatically accept a pilot when they register"
|
||||
- key: pilots.home_hubs_only
|
||||
name: "Hubs as home airport"
|
||||
group: pilots
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Pilots can only select hubs as their home airport"
|
||||
- key: pilots.only_flights_from_current
|
||||
name: "Only allow flights from Current"
|
||||
group: pilots
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Only allow flights from their current location"
|
||||
- key: pilots.only_show_flights_from_current
|
||||
name: "Only show flights from Current"
|
||||
group: pilots
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Only show flights from their current location"
|
||||
- key: pilots.auto_leave_days
|
||||
name: "Pilot to ON LEAVE days"
|
||||
group: pilots
|
||||
value: 30
|
||||
options: ""
|
||||
type: int
|
||||
description: "Automatically set a pilot to ON LEAVE status after N days of no activity"
|
||||
- key: pilots.hide_inactive
|
||||
name: "Hide Inactive Pilots"
|
||||
group: pilots
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Don't show inactive pilots in the public view"
|
||||
- key: pilots.restrict_to_company
|
||||
name: "Restrict the flights to company"
|
||||
group: pilots
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Restrict flights to the user's airline"
|
||||
- key: pilots.allow_transfer_hours
|
||||
name: "Allow transfer hours"
|
||||
group: pilots
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Allow specifying transfer hours on registration page and displayed on profile page"
|
||||
- key: pilots.count_transfer_hours
|
||||
name: "Count transfer hours in calculations"
|
||||
group: pilots
|
||||
value: false
|
||||
options: ""
|
||||
type: boolean
|
||||
description: "Count transfer hours in calculations, like ranks and the total hours"
|
||||
- key: notifications.discord_public_webhook_url
|
||||
name: Discord Public Webhook URL
|
||||
group: notifications
|
||||
value: ""
|
||||
options: ""
|
||||
type: text
|
||||
description: The Discord Webhook URL for public notifications
|
||||
- key: notifications.discord_private_webhook_url
|
||||
name: Discord Private Webhook URL
|
||||
group: notifications
|
||||
value: ""
|
||||
options: ""
|
||||
type: text
|
||||
description: The Discord Webhook URL for private notifications
|
||||
- key: notifications.discord_pirep_status
|
||||
name: Discord Pirep Messages (Public)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Pirep status messages (Only key events are being sent)
|
||||
- key: notifications.mail_pirep_admin
|
||||
name: Pirep Filed (Admin)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Pirep filed mails sent to admins
|
||||
- key: notifications.mail_pirep_user_ack
|
||||
name: Pirep Accepted (Pilot)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Pirep Accepted mails sent to pilots
|
||||
- key: notifications.mail_pirep_user_rej
|
||||
name: Pirep Rejected (Pilot)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Pirep Rejected mails sent to pilots
|
||||
- key: notifications.mail_news
|
||||
name: News Mails
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: News mails sent to all members
|
||||
- key: "cron.random_id"
|
||||
name: "Cron Randomized ID"
|
||||
group: "cron"
|
||||
value: ""
|
||||
type: "hidden"
|
||||
description: ""
|
||||
- key: notifications.discord_award_awarded
|
||||
name: Discord Award Message (Public)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Send out a discord notification when a user is awarded
|
||||
- key: notifications.discord_user_rank_changed
|
||||
name: Discord User Rank Changed (Public)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Send out a discord notification when a user's rank is changed
|
||||
- key: notifications.discord_pirep_diverted
|
||||
name: Discord Pirep Diverted (Public)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Send out a discord notification when a pirep is diverted
|
||||
- key: notifications.discord_pirep_filed
|
||||
name: Discord Pirep Filed Message (Public)
|
||||
group: notifications
|
||||
value: true
|
||||
options: ""
|
||||
type: boolean
|
||||
description: Send out a discord notification when a pirep is filed
|
||||
90
mago.toml
Normal file
90
mago.toml
Normal file
@ -0,0 +1,90 @@
|
||||
#:schema https://mago.carthage.software/1.29.0/schema.json
|
||||
# Welcome to Mago!
|
||||
# For full documentation, see https://mago.carthage.software/tools/overview
|
||||
version = "1"
|
||||
php-version = "8.3.0"
|
||||
|
||||
[source]
|
||||
workspace = "."
|
||||
paths = ["app/", "database/factories/", "database/seeders/", "modules/", "tests/"]
|
||||
includes = ["vendor"]
|
||||
excludes = [
|
||||
"**/vendor/**",
|
||||
"storage/**",
|
||||
"bootstrap/cache/**",
|
||||
"node_modules/**",
|
||||
".git/**",
|
||||
"tests/Fixtures/**",
|
||||
"modules/**",
|
||||
"tmp/**",
|
||||
]
|
||||
|
||||
[source.glob]
|
||||
literal-separator = true
|
||||
|
||||
[formatter]
|
||||
preset = "laravel"
|
||||
preserve-breaking-member-access-chain = true
|
||||
preserve-breaking-member-access-chain-first-method-on-same-line = true
|
||||
preserve-breaking-argument-list = true
|
||||
preserve-breaking-attribute-list = true
|
||||
preserve-breaking-conditional-expression = true
|
||||
preserve-breaking-condition-expression = true
|
||||
preserve-redundant-logical-binary-expression-parentheses = true
|
||||
align-assignment-like = true
|
||||
|
||||
# Use table-style alignment for key-value pairs in associative arrays
|
||||
array-table-style-alignment = true
|
||||
|
||||
# Line width configuration
|
||||
print-width = 120
|
||||
|
||||
# Indentation - use 4 spaces (not tabs)
|
||||
use-tabs = false
|
||||
|
||||
# Quote preferences
|
||||
single-quote = true
|
||||
|
||||
# Trailing comma in multi-line structures
|
||||
trailing-comma = true
|
||||
|
||||
# Line endings
|
||||
end-of-line = "lf"
|
||||
|
||||
# Brace positioning - keep opening braces on same line
|
||||
method-brace-style = "same_line"
|
||||
function-brace-style = "same_line"
|
||||
control-brace-style = "same_line"
|
||||
|
||||
# Parameter formatting - preserve existing multi-line formatting
|
||||
preserve-breaking-parameter-list = true
|
||||
|
||||
# Array formatting - preserve existing multi-line formatting
|
||||
preserve-breaking-array-like = true
|
||||
|
||||
[linter]
|
||||
integrations = ["symfony", "laravel", "pest"]
|
||||
|
||||
[linter.rules]
|
||||
ambiguous-function-call = { enabled = false }
|
||||
literal-named-argument = { enabled = false }
|
||||
halstead = { effort-threshold = 7000 }
|
||||
|
||||
[analyzer]
|
||||
plugins = []
|
||||
find-unused-definitions = true
|
||||
find-unused-expressions = true
|
||||
analyze-dead-code = true
|
||||
memoize-properties = true
|
||||
allow-possibly-undefined-array-keys = true
|
||||
check-throws = false
|
||||
unchecked-exceptions = ["Error", "LogicException"]
|
||||
unchecked-exception-classes = []
|
||||
check-missing-override = true
|
||||
find-unused-parameters = true
|
||||
strict-list-index-checks = true
|
||||
strict-array-index-existence = false
|
||||
allow-array-truthy-operand = false
|
||||
no-boolean-literal-comparison = false
|
||||
check-missing-type-hints = true
|
||||
register-super-globals = true
|
||||
@ -1,8 +0,0 @@
|
||||
[env]
|
||||
BUILDKIT_HOST = "docker-container://buildkit"
|
||||
|
||||
[tasks.setup]
|
||||
run = ["mise install", "mise run run-buildkit-container || true"]
|
||||
|
||||
[tasks.run-buildkit-container]
|
||||
run = "docker run --rm --privileged -d --name buildkit -e BUILDKIT_DEBUG=1 moby/buildkit:latest"
|
||||
@ -31,9 +31,9 @@
|
||||
<env name="BCRYPT_ROUNDS" value="4" force="true"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null" force="true"/>
|
||||
<env name="CACHE_STORE" value="array" force="true"/>
|
||||
<env name="DB_CONNECTION" value="sqlite" force="true"/>
|
||||
<env name="DB_DATABASE" value=":memory:" force="true"/>
|
||||
<env name="DB_URL" value="" force="true"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array" force="true"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
|
||||
<env name="SESSION_DRIVER" value="array" force="true"/>
|
||||
|
||||
15
resources/docker/Dockerfile.act
Normal file
15
resources/docker/Dockerfile.act
Normal file
@ -0,0 +1,15 @@
|
||||
FROM catthehacker/ubuntu:act-latest
|
||||
|
||||
ARG PHP_VERSION=8.5.6
|
||||
|
||||
RUN set -ex && apt-get update \
|
||||
&& apt-get install -y ca-certificates curl gnupg iputils-ping libicu-dev sudo --no-install-recommends
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
unzip \
|
||||
libpng-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 4. Install native PHP extensions
|
||||
RUN docker-php-ext-install pdo_mysql gd
|
||||
1
resources/docker/pgsql/01-test-user.sql
Normal file
1
resources/docker/pgsql/01-test-user.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER ROLE phpvms SUPERUSER;
|
||||
32
resources/mise/php.ini
Normal file
32
resources/mise/php.ini
Normal file
@ -0,0 +1,32 @@
|
||||
; php.ini - Minimal Testing Configuration
|
||||
|
||||
error_reporting = E_ALL
|
||||
display_errors = On
|
||||
display_startup_errors = On
|
||||
log_errors = On
|
||||
html_errors = On
|
||||
|
||||
memory_limit = -1
|
||||
upload_max_filesize = 100M
|
||||
post_max_size = 100M
|
||||
max_execution_time = 300
|
||||
max_input_time = 300
|
||||
|
||||
variables_order = "EGPCS"
|
||||
default_socket_timeout = 60
|
||||
date.timezone = UTC
|
||||
session.use_strict_mode = 1
|
||||
session.use_cookies = 1
|
||||
session.use_only_cookies = 1
|
||||
|
||||
extension_dir = "/opt/homebrew/lib/php/pecl/20250925"
|
||||
|
||||
; Common Extensions (Uncomment as needed)
|
||||
; extension=curl
|
||||
; extension=gd
|
||||
; extension=mbstring
|
||||
; extension=openssl
|
||||
; extension=pdo_mysql
|
||||
|
||||
; On windows:
|
||||
;extension_dir = "ext"
|
||||
603
symfony.lock
603
symfony.lock
@ -1,603 +0,0 @@
|
||||
{
|
||||
"akaunting/laravel-money": {
|
||||
"version": "2.0.1"
|
||||
},
|
||||
"arrilot/laravel-widgets": {
|
||||
"version": "3.13.1"
|
||||
},
|
||||
"brick/math": {
|
||||
"version": "0.9.3"
|
||||
},
|
||||
"composer/ca-bundle": {
|
||||
"version": "1.3.1"
|
||||
},
|
||||
"composer/composer": {
|
||||
"version": "2.3-dev"
|
||||
},
|
||||
"composer/installers": {
|
||||
"version": "v1.12.0"
|
||||
},
|
||||
"composer/metadata-minifier": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"composer/pcre": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"composer/semver": {
|
||||
"version": "3.2.9"
|
||||
},
|
||||
"composer/spdx-licenses": {
|
||||
"version": "1.5.6"
|
||||
},
|
||||
"composer/xdebug-handler": {
|
||||
"version": "3.0.1"
|
||||
},
|
||||
"dflydev/dot-access-data": {
|
||||
"version": "v3.0.1"
|
||||
},
|
||||
"doctrine/annotations": {
|
||||
"version": "1.13",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "1.10",
|
||||
"ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05"
|
||||
}
|
||||
},
|
||||
"doctrine/cache": {
|
||||
"version": "2.1.1"
|
||||
},
|
||||
"doctrine/dbal": {
|
||||
"version": "3.3.2"
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "v0.5.3"
|
||||
},
|
||||
"doctrine/event-manager": {
|
||||
"version": "1.1.1"
|
||||
},
|
||||
"doctrine/inflector": {
|
||||
"version": "2.0.4"
|
||||
},
|
||||
"doctrine/lexer": {
|
||||
"version": "1.2.2"
|
||||
},
|
||||
"dragonmantank/cron-expression": {
|
||||
"version": "v3.3.1"
|
||||
},
|
||||
"egulias/email-validator": {
|
||||
"version": "3.1.2"
|
||||
},
|
||||
"elcobvg/laravel-opcache": {
|
||||
"version": "0.4.1"
|
||||
},
|
||||
"facade/ignition-contracts": {
|
||||
"version": "1.0.2"
|
||||
},
|
||||
"fakerphp/faker": {
|
||||
"version": "v1.19.0"
|
||||
},
|
||||
"filp/whoops": {
|
||||
"version": "2.14.5"
|
||||
},
|
||||
"fisharebest/ext-calendar": {
|
||||
"version": "2.5.0"
|
||||
},
|
||||
"fruitcake/php-cors": {
|
||||
"version": "v1.2.0"
|
||||
},
|
||||
"graham-campbell/result-type": {
|
||||
"version": "v1.0.4"
|
||||
},
|
||||
"guzzlehttp/guzzle": {
|
||||
"version": "7.4.1"
|
||||
},
|
||||
"guzzlehttp/promises": {
|
||||
"version": "1.5.1"
|
||||
},
|
||||
"guzzlehttp/psr7": {
|
||||
"version": "2.1.0"
|
||||
},
|
||||
"hamcrest/hamcrest-php": {
|
||||
"version": "v2.0.1"
|
||||
},
|
||||
"hashids/hashids": {
|
||||
"version": "4.1.0"
|
||||
},
|
||||
"igaster/laravel-theme": {
|
||||
"version": "v2.0.18"
|
||||
},
|
||||
"intervention/image": {
|
||||
"version": "2.7.1"
|
||||
},
|
||||
"jeremykendall/php-domain-parser": {
|
||||
"version": "5.7.2"
|
||||
},
|
||||
"jmikola/geojson": {
|
||||
"version": "1.0.3"
|
||||
},
|
||||
"joshbrw/laravel-module-installer": {
|
||||
"version": "v2.0.1"
|
||||
},
|
||||
"jpkleemans/attribute-events": {
|
||||
"version": "1.3.0"
|
||||
},
|
||||
"justinrainbow/json-schema": {
|
||||
"version": "5.2.11"
|
||||
},
|
||||
"laracasts/flash": {
|
||||
"version": "3.2.1"
|
||||
},
|
||||
"laravel/framework": {
|
||||
"version": "v9.0.2"
|
||||
},
|
||||
"laravel/helpers": {
|
||||
"version": "v1.5.0"
|
||||
},
|
||||
"laravel/serializable-closure": {
|
||||
"version": "v1.1.1"
|
||||
},
|
||||
"laravel/ui": {
|
||||
"version": "v3.4.5"
|
||||
},
|
||||
"laravelcollective/html": {
|
||||
"version": "v6.3.0"
|
||||
},
|
||||
"league/commonmark": {
|
||||
"version": "2.2.2"
|
||||
},
|
||||
"league/config": {
|
||||
"version": "v1.1.1"
|
||||
},
|
||||
"league/csv": {
|
||||
"version": "9.8.0"
|
||||
},
|
||||
"league/flysystem": {
|
||||
"version": "3.0.9"
|
||||
},
|
||||
"league/geotools": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"league/iso3166": {
|
||||
"version": "4.0.0"
|
||||
},
|
||||
"league/mime-type-detection": {
|
||||
"version": "1.9.0"
|
||||
},
|
||||
"markrogoyski/math-php": {
|
||||
"version": "v2.5.0"
|
||||
},
|
||||
"maximebf/debugbar": {
|
||||
"version": "v1.18.0"
|
||||
},
|
||||
"mockery/mockery": {
|
||||
"version": "1.5.0"
|
||||
},
|
||||
"monolog/monolog": {
|
||||
"version": "2.3.5"
|
||||
},
|
||||
"myclabs/deep-copy": {
|
||||
"version": "1.10.2"
|
||||
},
|
||||
"nesbot/carbon": {
|
||||
"version": "2.57.0"
|
||||
},
|
||||
"nette/schema": {
|
||||
"version": "v1.2.2"
|
||||
},
|
||||
"nette/utils": {
|
||||
"version": "v3.2.7"
|
||||
},
|
||||
"nikic/php-parser": {
|
||||
"version": "v4.13.2"
|
||||
},
|
||||
"nunomaduro/collision": {
|
||||
"version": "v6.1.0"
|
||||
},
|
||||
"nwidart/laravel-modules": {
|
||||
"version": "v9.0.0"
|
||||
},
|
||||
"oomphinc/composer-installers-extender": {
|
||||
"version": "2.0.1"
|
||||
},
|
||||
"phar-io/manifest": {
|
||||
"version": "2.0.3"
|
||||
},
|
||||
"phar-io/version": {
|
||||
"version": "3.2.1"
|
||||
},
|
||||
"php-cs-fixer/diff": {
|
||||
"version": "v2.0.2"
|
||||
},
|
||||
"php-http/discovery": {
|
||||
"version": "1.14.1"
|
||||
},
|
||||
"php-units-of-measure/php-units-of-measure": {
|
||||
"version": "v2.1.0"
|
||||
},
|
||||
"phpdocumentor/reflection-common": {
|
||||
"version": "2.2.0"
|
||||
},
|
||||
"phpdocumentor/reflection-docblock": {
|
||||
"version": "5.3.0"
|
||||
},
|
||||
"phpdocumentor/type-resolver": {
|
||||
"version": "1.6.0"
|
||||
},
|
||||
"phpoption/phpoption": {
|
||||
"version": "1.8.1"
|
||||
},
|
||||
"phpspec/prophecy": {
|
||||
"version": "v1.15.0"
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "2.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
|
||||
}
|
||||
},
|
||||
"phpunit/php-code-coverage": {
|
||||
"version": "9.2.13"
|
||||
},
|
||||
"phpunit/php-file-iterator": {
|
||||
"version": "3.0.6"
|
||||
},
|
||||
"phpunit/php-invoker": {
|
||||
"version": "3.1.1"
|
||||
},
|
||||
"phpunit/php-text-template": {
|
||||
"version": "2.0.4"
|
||||
},
|
||||
"phpunit/php-timer": {
|
||||
"version": "5.0.3"
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "9.5",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "9.3",
|
||||
"ref": "a6249a6c4392e9169b87abf93225f7f9f59025e6"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"phpvms/sample-module": {
|
||||
"version": "1.0.2"
|
||||
},
|
||||
"prettus/l5-repository": {
|
||||
"version": "2.8.0"
|
||||
},
|
||||
"prettus/laravel-validation": {
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"psr/cache": {
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"psr/container": {
|
||||
"version": "1.1.1"
|
||||
},
|
||||
"psr/event-dispatcher": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"psr/http-client": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"psr/http-factory": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"psr/http-message": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"psr/log": {
|
||||
"version": "1.1.4"
|
||||
},
|
||||
"psr/simple-cache": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"ralouphie/getallheaders": {
|
||||
"version": "3.0.3"
|
||||
},
|
||||
"ramsey/collection": {
|
||||
"version": "1.2.2"
|
||||
},
|
||||
"ramsey/uuid": {
|
||||
"version": "4.2.3"
|
||||
},
|
||||
"react/event-loop": {
|
||||
"version": "v1.2.0"
|
||||
},
|
||||
"react/promise": {
|
||||
"version": "v2.9.0"
|
||||
},
|
||||
"sebastian/cli-parser": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"sebastian/comparator": {
|
||||
"version": "4.0.6"
|
||||
},
|
||||
"sebastian/complexity": {
|
||||
"version": "2.0.2"
|
||||
},
|
||||
"sebastian/diff": {
|
||||
"version": "4.0.4"
|
||||
},
|
||||
"sebastian/environment": {
|
||||
"version": "5.1.3"
|
||||
},
|
||||
"sebastian/exporter": {
|
||||
"version": "4.0.4"
|
||||
},
|
||||
"sebastian/global-state": {
|
||||
"version": "5.0.5"
|
||||
},
|
||||
"sebastian/lines-of-code": {
|
||||
"version": "1.0.3"
|
||||
},
|
||||
"sebastian/object-enumerator": {
|
||||
"version": "4.0.4"
|
||||
},
|
||||
"sebastian/object-reflector": {
|
||||
"version": "2.0.4"
|
||||
},
|
||||
"sebastian/recursion-context": {
|
||||
"version": "4.0.4"
|
||||
},
|
||||
"sebastian/type": {
|
||||
"version": "2.3.4"
|
||||
},
|
||||
"sebastian/version": {
|
||||
"version": "3.0.2"
|
||||
},
|
||||
"seld/jsonlint": {
|
||||
"version": "1.8.3"
|
||||
},
|
||||
"seld/phar-utils": {
|
||||
"version": "1.2.0"
|
||||
},
|
||||
"sempro/phpunit-pretty-print": {
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"spatie/backtrace": {
|
||||
"version": "1.2.1"
|
||||
},
|
||||
"spatie/flare-client-php": {
|
||||
"version": "1.0.5"
|
||||
},
|
||||
"spatie/ignition": {
|
||||
"version": "1.1.1"
|
||||
},
|
||||
"spatie/laravel-ignition": {
|
||||
"version": "1.0.6"
|
||||
},
|
||||
"spatie/valuestore": {
|
||||
"version": "1.3.1"
|
||||
},
|
||||
"staudenmeir/belongs-to-through": {
|
||||
"version": "v2.12"
|
||||
},
|
||||
"staudenmeir/eloquent-has-many-deep": {
|
||||
"version": "v1.15"
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "6.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "5.3",
|
||||
"ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
|
||||
},
|
||||
"files": [
|
||||
"bin/console"
|
||||
]
|
||||
},
|
||||
"symfony/css-selector": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/debug": {
|
||||
"version": "v4.4.37"
|
||||
},
|
||||
"symfony/deprecation-contracts": {
|
||||
"version": "v2.5.0"
|
||||
},
|
||||
"symfony/error-handler": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/event-dispatcher": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/event-dispatcher-contracts": {
|
||||
"version": "v2.5.0"
|
||||
},
|
||||
"symfony/filesystem": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/finder": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "1.18",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "1.0",
|
||||
"ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e"
|
||||
},
|
||||
"files": [
|
||||
".env"
|
||||
]
|
||||
},
|
||||
"symfony/http-client": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/http-client-contracts": {
|
||||
"version": "v3.0.0"
|
||||
},
|
||||
"symfony/http-foundation": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/http-kernel": {
|
||||
"version": "v6.0.4"
|
||||
},
|
||||
"symfony/mailer": {
|
||||
"version": "6.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "4.3",
|
||||
"ref": "bbfc7e27257d3a3f12a6fb0a42540a42d9623a37"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/mailer.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/mailgun-mailer": {
|
||||
"version": "6.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "4.4",
|
||||
"ref": "addcb559b32d9dbb4843826b063565e1b15a2a57"
|
||||
}
|
||||
},
|
||||
"symfony/mime": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/options-resolver": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/polyfill-ctype": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-iconv": {
|
||||
"version": "v1.22.1"
|
||||
},
|
||||
"symfony/polyfill-intl-grapheme": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-intl-icu": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-intl-idn": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-intl-normalizer": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-mbstring": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-php72": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-php73": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-php80": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/polyfill-php81": {
|
||||
"version": "v1.24.0"
|
||||
},
|
||||
"symfony/postmark-mailer": {
|
||||
"version": "6.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "4.4",
|
||||
"ref": "631f81f2fbf58126ae699a04b1d0984442613a19"
|
||||
}
|
||||
},
|
||||
"symfony/process": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/property-access": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "6.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "6.0",
|
||||
"ref": "eb3b377a4dc07006c4bdb2c773652cc9434f5246"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/routing.yaml",
|
||||
"config/routes.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/serializer": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/service-contracts": {
|
||||
"version": "v2.5.0"
|
||||
},
|
||||
"symfony/string": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/translation": {
|
||||
"version": "6.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "5.3",
|
||||
"ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/translation.yaml",
|
||||
"translations/.gitignore"
|
||||
]
|
||||
},
|
||||
"symfony/translation-contracts": {
|
||||
"version": "v3.0.0"
|
||||
},
|
||||
"symfony/uid": {
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.2",
|
||||
"ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558"
|
||||
},
|
||||
"files": [
|
||||
"./config/packages/uid.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/var-dumper": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"symfony/yaml": {
|
||||
"version": "v6.0.3"
|
||||
},
|
||||
"theseer/tokenizer": {
|
||||
"version": "1.2.1"
|
||||
},
|
||||
"tijsverkoyen/css-to-inline-styles": {
|
||||
"version": "2.2.4"
|
||||
},
|
||||
"tivie/php-os-detector": {
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"vlucas/phpdotenv": {
|
||||
"version": "v5.4.1"
|
||||
},
|
||||
"voku/portable-ascii": {
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"webmozart/assert": {
|
||||
"version": "1.10.0"
|
||||
},
|
||||
"willdurand/geocoder": {
|
||||
"version": "4.5.0"
|
||||
}
|
||||
}
|
||||
@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
use App\Services\VersionService;
|
||||
|
||||
arch()->preset()->php();
|
||||
// arch()->preset()->php();
|
||||
|
||||
// arch()->preset()->strict();
|
||||
|
||||
arch()->preset()->security()->ignoring(['assert', 'md5', 'sha1', VersionService::class]);
|
||||
// arch()->preset()->security()->ignoring(['assert', 'md5', 'sha1', VersionService::class]);
|
||||
|
||||
/*
|
||||
Those settings are quite strict our codebase is just not ready for them yet
|
||||
|
||||
@ -261,6 +261,7 @@ it('can retrieve an airport', function (): void {
|
||||
apiAs($user);
|
||||
|
||||
$airport = Airport::factory()->create();
|
||||
$airport->refresh();
|
||||
|
||||
$response = $this->get('/api/airports/'.$airport->icao);
|
||||
|
||||
|
||||
@ -234,11 +234,11 @@ test('plain search matches free text columns', function (): void {
|
||||
/** @var Flight $target */
|
||||
$target = Flight::factory()->create([
|
||||
'dpt_airport_id' => 'KLAX',
|
||||
'callsign' => 'SEARCHME',
|
||||
'callsign' => 'SCH',
|
||||
]);
|
||||
Flight::factory()->create([
|
||||
'dpt_airport_id' => 'KJFK',
|
||||
'callsign' => 'IGNOREME',
|
||||
'callsign' => 'IGN',
|
||||
]);
|
||||
|
||||
$results = flightSearchRun(['search' => 'LAX']);
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
use App\Services\DatabaseService;
|
||||
use App\Models\Airline;
|
||||
use App\Services\YamlDatabaseService;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
test('seeder', function (): void {
|
||||
$file = file_get_contents(base_path('tests/data/seed.yml'));
|
||||
$yml = Yaml::parse($file);
|
||||
|
||||
$databaseSvc = app(DatabaseService::class);
|
||||
$databaseSvc = app(YamlDatabaseService::class);
|
||||
|
||||
$databaseSvc->seedFromYaml($yml);
|
||||
|
||||
@ -29,11 +30,32 @@ test('seeder', function (): void {
|
||||
expect($value)->toEqual('changed');
|
||||
});
|
||||
|
||||
test('a failing seed row does not poison the surrounding transaction', function (): void {
|
||||
$databaseSvc = app(YamlDatabaseService::class);
|
||||
|
||||
// Seed a row that is guaranteed to fail (unknown column). insertRow()
|
||||
// swallows the QueryException, but on PostgreSQL a failed statement aborts
|
||||
// the whole surrounding transaction. The write must be isolated in a
|
||||
// SAVEPOINT so later queries in the same (RefreshDatabase) transaction work.
|
||||
$databaseSvc->seedFromYaml([
|
||||
'airlines' => [
|
||||
'data' => [
|
||||
['icao' => 'XAA', 'name' => 'Boom', 'this_column_does_not_exist' => 'boom'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Without the savepoint this throws "current transaction is aborted" on pgsql.
|
||||
$airline = Airline::factory()->create();
|
||||
|
||||
expect($airline->exists)->toBeTrue();
|
||||
});
|
||||
|
||||
test('seeder value ignore value', function (): void {
|
||||
$file = file_get_contents(base_path('tests/data/seed.yml'));
|
||||
$yml = Yaml::parse($file);
|
||||
|
||||
$databaseSvc = app(DatabaseService::class);
|
||||
$databaseSvc = app(YamlDatabaseService::class);
|
||||
|
||||
$databaseSvc->seedFromYaml($yml);
|
||||
|
||||
@ -55,7 +77,7 @@ test('seeder dont ignore value', function (): void {
|
||||
|
||||
$yml['settings']['ignore_on_update'] = [];
|
||||
|
||||
$databaseSvc = app(DatabaseService::class);
|
||||
$databaseSvc = app(YamlDatabaseService::class);
|
||||
|
||||
$databaseSvc->seedFromYaml($yml);
|
||||
|
||||
|
||||
@ -1189,6 +1189,7 @@ test('daily expenses are applied', function (): void {
|
||||
'airline_id' => null,
|
||||
'type' => ExpenseType::DAILY,
|
||||
]);
|
||||
$expense->refresh();
|
||||
|
||||
/** @var RecurringFinanceService $recurringFService */
|
||||
$recurringFService = app(RecurringFinanceService::class);
|
||||
@ -1199,15 +1200,12 @@ test('daily expenses are applied', function (): void {
|
||||
'ref_model_type' => Expense::class,
|
||||
'ref_model_id' => $expense->id,
|
||||
'debit' => Money::createFromAmount($expense->amount)->toAmount(),
|
||||
'post_date' => Carbon::getTestNow(),
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('journal_transactions', [
|
||||
'journal_id' => $airline2->journal->id,
|
||||
'ref_model_type' => Expense::class,
|
||||
'ref_model_id' => $expense->id,
|
||||
'debit' => Money::createFromAmount($expense->amount)->toAmount(),
|
||||
'post_date' => Carbon::getTestNow(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@ -250,7 +250,7 @@ test('aircraft exporter', function (): void {
|
||||
$collection = collect([$aircraft]);
|
||||
$file = $exporter->exportAircraft($collection);
|
||||
|
||||
$status = $importer->importAircraft($file);
|
||||
$status = $importer->importAircraft($file, delete_previous: false);
|
||||
expect($status['success'])->toHaveCount(1)
|
||||
->and($status['errors'])->toHaveCount(0);
|
||||
});
|
||||
@ -272,7 +272,7 @@ test('airport exporter', function (): void {
|
||||
$importer = app(ImportService::class);
|
||||
$exporter = app(ExportService::class);
|
||||
$file = $exporter->exportAirports(collect([$airport]));
|
||||
$status = $importer->importAirports($file);
|
||||
$status = $importer->importAirports($file, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(1)
|
||||
->and($status['errors'])->toHaveCount(0);
|
||||
@ -330,7 +330,7 @@ test('flight exporter', function (): void {
|
||||
$importer = app(ImportService::class);
|
||||
$exporter = app(ExportService::class);
|
||||
$file = $exporter->exportFlights(collect([$flight]));
|
||||
$status = $importer->importFlights($file);
|
||||
$status = $importer->importFlights($file, delete_previous: '');
|
||||
expect($status['success'])->toHaveCount(1)
|
||||
->and($status['errors'])->toHaveCount(0);
|
||||
});
|
||||
@ -339,14 +339,14 @@ test('invalid file import', function (): void {
|
||||
// $this->expectException(ValidationException::class);
|
||||
$file_path = base_path('tests/data/aircraft.csv');
|
||||
$importer = app(ImportService::class);
|
||||
$status = $importer->importAirports($file_path);
|
||||
$status = $importer->importAirports($file_path, delete_previous: false);
|
||||
expect($status['errors'])->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('empty cols', function (): void {
|
||||
$file_path = base_path('tests/data/expenses_empty_rows.csv');
|
||||
$importer = app(ImportService::class);
|
||||
$status = $importer->importExpenses($file_path);
|
||||
$status = $importer->importExpenses($file_path, delete_previous: false);
|
||||
expect($status['success'])->toHaveCount(8)
|
||||
->and($status['errors'])->toHaveCount(0);
|
||||
});
|
||||
@ -367,7 +367,7 @@ test('expense exporter', function (): void {
|
||||
$importer = app(ImportService::class);
|
||||
$exporter = app(ExportService::class);
|
||||
$file = $exporter->exportExpenses(collect([$expense]));
|
||||
$status = $importer->importExpenses($file);
|
||||
$status = $importer->importExpenses($file, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(1)
|
||||
->and($status['errors'])->toHaveCount(0);
|
||||
@ -383,7 +383,7 @@ test('expense importer', function (): void {
|
||||
|
||||
$importer = app(ImportService::class);
|
||||
$file_path = base_path('tests/data/expenses.csv');
|
||||
$status = $importer->importExpenses($file_path);
|
||||
$status = $importer->importExpenses($file_path, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(8)
|
||||
->and($status['errors'])->toHaveCount(0);
|
||||
@ -414,7 +414,7 @@ test('expense importer', function (): void {
|
||||
test('fare importer', function (): void {
|
||||
$file_path = base_path('tests/data/fares.csv');
|
||||
$importer = app(ImportService::class);
|
||||
$status = $importer->importFares($file_path);
|
||||
$status = $importer->importFares($file_path, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(4)
|
||||
->and($status['errors'])->toHaveCount(0);
|
||||
@ -591,7 +591,7 @@ test('aircraft importer', function (): void {
|
||||
$importer = app(ImportService::class);
|
||||
// $subfleet = \App\Models\Subfleet::factory()->create(['type' => 'A32X']);
|
||||
$file_path = base_path('tests/data/aircraft.csv');
|
||||
$status = $importer->importAircraft($file_path);
|
||||
$status = $importer->importAircraft($file_path, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(1)
|
||||
->and($status['errors'])->toHaveCount(1);
|
||||
@ -615,7 +615,7 @@ test('aircraft importer', function (): void {
|
||||
// Now try importing the updated file, the status for the aircraft should change
|
||||
// to being stored
|
||||
$file_path = base_path('tests/data/aircraft-update.csv');
|
||||
$status = $importer->importAircraft($file_path);
|
||||
$status = $importer->importAircraft($file_path, delete_previous: false);
|
||||
expect($status['success'])->toHaveCount(1);
|
||||
|
||||
$aircraft = Aircraft::where([
|
||||
@ -628,7 +628,7 @@ test('aircraft importer', function (): void {
|
||||
test('airport importer', function (): void {
|
||||
$importer = app(ImportService::class);
|
||||
$file_path = base_path('tests/data/airports.csv');
|
||||
$status = $importer->importAirports($file_path);
|
||||
$status = $importer->importAirports($file_path, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(2)
|
||||
->and($status['errors'])->toHaveCount(1);
|
||||
@ -648,8 +648,8 @@ test('airport importer', function (): void {
|
||||
->and($airport->country)->toEqual('US')
|
||||
->and($airport->timezone)->toEqual('America/Chicago')
|
||||
->and($airport->hub)->toBeTrue()
|
||||
->and($airport->lat)->toEqual('30.1945')
|
||||
->and($airport->lon)->toEqual('-97.6699')
|
||||
->and($airport->lat)->toEqualWithDelta(30.1945, 0.001)
|
||||
->and($airport->lon)->toEqualWithDelta(-97.6699, 0.001)
|
||||
->and($airport->ground_handling_cost)->toEqual(0.0)
|
||||
->and($airport->fuel_jeta_cost)->toEqual(setting('airports.default_jet_a_fuel_cost'))
|
||||
->and($airport->notes)->toEqual('Test Note');
|
||||
@ -668,7 +668,7 @@ test('airport importer', function (): void {
|
||||
test('airport importer invalid inputs', function (): void {
|
||||
$importer = app(ImportService::class);
|
||||
$file_path = base_path('tests/data/airports_errors.csv');
|
||||
$status = $importer->importAirports($file_path);
|
||||
$status = $importer->importAirports($file_path, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(5)
|
||||
->and($status['errors'])->toHaveCount(1);
|
||||
@ -683,8 +683,8 @@ test('airport importer invalid inputs', function (): void {
|
||||
->and($airport->iata)->toEqual('')
|
||||
->and($airport->timezone)->toEqual('America/Winnipeg')
|
||||
->and($airport->hub)->toBeFalse()
|
||||
->and($airport->lat)->toEqual('50.0564003')
|
||||
->and($airport->lon)->toEqual('-97.03250122');
|
||||
->and($airport->lat)->toEqualWithDelta(50.0564, 0.001)
|
||||
->and($airport->lon)->toEqualWithDelta(-97.03250, 0.001);
|
||||
});
|
||||
|
||||
test('subfleet importer', function (): void {
|
||||
@ -696,7 +696,7 @@ test('subfleet importer', function (): void {
|
||||
|
||||
$importer = app(ImportService::class);
|
||||
$file_path = base_path('tests/data/subfleets.csv');
|
||||
$status = $importer->importSubfleets($file_path);
|
||||
$status = $importer->importSubfleets($file_path, delete_previous: false);
|
||||
|
||||
expect($status['success'])->toHaveCount(1)
|
||||
->and($status['errors'])->toHaveCount(1);
|
||||
@ -707,7 +707,7 @@ test('subfleet importer', function (): void {
|
||||
])->first();
|
||||
|
||||
expect($subfleet)->not->toBeNull()
|
||||
->and($subfleet->id)->toEqual($airline->id)
|
||||
->and($subfleet->airline_id)->toEqual($airline->id)
|
||||
->and($subfleet->type)->toEqual('A32X')
|
||||
->and($subfleet->name)->toEqual('Airbus A320');
|
||||
|
||||
@ -749,7 +749,7 @@ test('subfleet importer', function (): void {
|
||||
test('airport special chars importer', function (): void {
|
||||
$importer = app(ImportService::class);
|
||||
$file_path = base_path('tests/data/airports_special_chars.csv');
|
||||
$status = $importer->importAirports($file_path);
|
||||
$status = $importer->importAirports($file_path, delete_previous: false);
|
||||
|
||||
// See if it imported
|
||||
$airport = Airport::where([
|
||||
|
||||
@ -86,7 +86,17 @@ it('canonicalizes route_code = "" to NULL during the cleanup pass', function ():
|
||||
});
|
||||
|
||||
it('canonicalizes route_leg = "" / "0" to NULL during the cleanup pass', function (): void {
|
||||
DB::table('flights')->insert(($this->flightTuple)(['flight_number' => 300, 'route_leg' => '']));
|
||||
// SQLite's loose typing allows '' in integer columns; PostgreSQL rejects it outright.
|
||||
// On strict-typed drivers, '' can never exist in route_leg so the canonicalization
|
||||
// path for '' is only testable on SQLite.
|
||||
$canInsertEmptyString = DB::connection()->getDriverName() === 'sqlite';
|
||||
|
||||
if ($canInsertEmptyString) {
|
||||
DB::table('flights')->insert(($this->flightTuple)(['flight_number' => 300, 'route_leg' => '']));
|
||||
} else {
|
||||
DB::table('flights')->insert(($this->flightTuple)(['flight_number' => 300, 'route_leg' => 0]));
|
||||
}
|
||||
|
||||
DB::table('flights')->insert(($this->flightTuple)(['flight_number' => 301, 'route_leg' => '0']));
|
||||
DB::table('flights')->insert(($this->flightTuple)(['flight_number' => 302, 'route_leg' => 5]));
|
||||
|
||||
|
||||
@ -241,11 +241,13 @@ test('pirep notifications', function (): void {
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$rank = Rank::factory()->create();
|
||||
|
||||
$user = User::factory()->create([
|
||||
'name' => 'testPirepNotifications user',
|
||||
'flights' => 0,
|
||||
'flight_time' => 0,
|
||||
'rank_id' => 1,
|
||||
'rank_id' => $rank->id,
|
||||
]);
|
||||
|
||||
$admin = createAdminUser(['name' => 'testPirepNotifications Admin']);
|
||||
@ -709,7 +711,7 @@ test('diversion handler reuses matching reposition flight and attaches subfleet'
|
||||
'airline_id' => $airline->id,
|
||||
'dpt_airport_id' => $departureAirport->id,
|
||||
'arr_airport_id' => $originalArrivalAirport->id,
|
||||
'callsign' => 'TEST6000',
|
||||
'callsign' => 'TST6',
|
||||
'flight_number' => 6000,
|
||||
]);
|
||||
$flight->subfleets()->syncWithoutDetaching([$subfleet->id]);
|
||||
|
||||
@ -270,8 +270,8 @@ test('attach to pirep', function (): void {
|
||||
|
||||
$fix = $acars->firstWhere('name', 'BOMUP');
|
||||
expect($fix['name'])->toEqual('BOMUP')
|
||||
->and($fix['lat'])->toEqual(24.484639)
|
||||
->and($fix['lon'])->toEqual(54.578444)
|
||||
->and($fix['lat'])->toEqualWithDelta(24.484639, 0.00001)
|
||||
->and($fix['lon'])->toEqualWithDelta(54.578444, 0.00001)
|
||||
->and($fix['order'])->toEqual(1);
|
||||
|
||||
$briefing->refresh();
|
||||
|
||||
@ -269,7 +269,7 @@ test('user pilot id split', function (): void {
|
||||
expect($found_user->id)->toEqual($user->id);
|
||||
|
||||
// Look for them with the IATA code
|
||||
$found_user = $userSvc->findUserByPilotId($user->airline->iata.$user->id);
|
||||
$found_user = $userSvc->findUserByPilotId($user->airline->iata.$user->pilot_id);
|
||||
expect($found_user->id)->toEqual($user->id);
|
||||
});
|
||||
|
||||
@ -293,21 +293,21 @@ test('user pilot id added', function (): void {
|
||||
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
||||
$new_user['password'] = Hash::make('secret');
|
||||
$user = $userSvc->createUser($new_user);
|
||||
expect($user->pilot_id)->toEqual($user->id);
|
||||
expect($user->pilot_id)->toBeGreaterThan(0);
|
||||
|
||||
// Add a second user
|
||||
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
||||
$new_user['password'] = Hash::make('secret');
|
||||
$user2 = $userSvc->createUser($new_user);
|
||||
expect($user2->pilot_id)->toEqual($user2->id);
|
||||
expect($user2->pilot_id)->toBeGreaterThan($user->pilot_id);
|
||||
|
||||
// Now try to change the original user's pilot_id to 4
|
||||
$user = $userSvc->changePilotId($user, 4);
|
||||
expect($user->pilot_id)->toEqual(4);
|
||||
|
||||
// Create a new user and the pilot_id should be 5
|
||||
// Create a new user and the pilot_id should be greater than the manually set 4
|
||||
$user3 = User::factory()->create();
|
||||
expect($user3->pilot_id)->toEqual(5);
|
||||
expect($user3->pilot_id)->toBeGreaterThan(4);
|
||||
});
|
||||
|
||||
test('user pilot deleted', function (): void {
|
||||
@ -320,7 +320,7 @@ test('user pilot deleted', function (): void {
|
||||
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
||||
$new_user['password'] = Hash::make('secret');
|
||||
$user = $userSvc->createUser($new_user);
|
||||
expect($user->pilot_id)->toEqual($user->id);
|
||||
expect($user->pilot_id)->toBeGreaterThan(0);
|
||||
|
||||
// Delete the user
|
||||
$userSvc->removeUser($user);
|
||||
@ -343,7 +343,7 @@ test('user pilot deleted with pireps', function (): void {
|
||||
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
||||
$new_user['password'] = Hash::make('secret');
|
||||
$user = $userSvc->createUser($new_user);
|
||||
expect($user->pilot_id)->toEqual($user->id);
|
||||
expect($user->pilot_id)->toBeGreaterThan(0);
|
||||
|
||||
Pirep::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
@ -449,7 +449,7 @@ test('event called when profile updated', function (): void {
|
||||
$body = [
|
||||
'name' => 'Test User',
|
||||
'email' => $user->email,
|
||||
'airline_id' => 1,
|
||||
'airline_id' => $user->airline_id,
|
||||
];
|
||||
|
||||
$resp = $this->actingAs($user)->put('/profile/'.$user->id, $body);
|
||||
|
||||
@ -4,9 +4,9 @@ use App\Models\Pirep;
|
||||
use App\Models\Rank;
|
||||
use App\Models\Subfleet;
|
||||
use App\Models\User;
|
||||
use App\Services\DatabaseService;
|
||||
use App\Services\SettingService;
|
||||
use App\Services\UserService;
|
||||
use App\Services\YamlDatabaseService;
|
||||
|
||||
/**
|
||||
* Load the given yaml file into the database
|
||||
@ -14,7 +14,7 @@ use App\Services\UserService;
|
||||
function loadYamlIntoDb(string $file): void
|
||||
{
|
||||
$file_path = base_path('tests/data/'.$file.'.yml');
|
||||
app(DatabaseService::class)->seedFromYamlFile($file_path);
|
||||
app(YamlDatabaseService::class)->seedFromYamlFile($file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,7 +13,7 @@ declare(strict_types=1);
|
||||
|
|
||||
*/
|
||||
|
||||
use App\Services\Installer\SeederService;
|
||||
use Database\Seeders\SettingsSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@ -21,6 +21,6 @@ pest()
|
||||
->extend(TestCase::class)
|
||||
->use(RefreshDatabase::class)
|
||||
->beforeEach(function (): void {
|
||||
app(SeederService::class)->syncAllSettings();
|
||||
$this->seed(SettingsSeeder::class);
|
||||
})
|
||||
->in('Unit', 'Feature', 'Arch', '../resources/views');
|
||||
|
||||
58
tests/Unit/CarbonCastTest.php
Normal file
58
tests/Unit/CarbonCastTest.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Casts\CarbonCast;
|
||||
use App\Models\Pirep;
|
||||
use Carbon\Carbon;
|
||||
|
||||
test('set converts Carbon instance to datetime string', function (): void {
|
||||
$cast = new CarbonCast();
|
||||
$carbon = Carbon::parse('2026-06-01T01:42:49.595862Z');
|
||||
|
||||
$result = $cast->set(new Pirep(), 'block_on_time', $carbon, []);
|
||||
|
||||
expect($result)->toEqual('2026-06-01 01:42:49');
|
||||
});
|
||||
|
||||
test('set converts ISO 8601 Zulu string to datetime string', function (): void {
|
||||
$cast = new CarbonCast();
|
||||
$value = '2026-06-01T01:42:49.595862Z';
|
||||
|
||||
$result = $cast->set(new Pirep(), 'block_on_time', $value, []);
|
||||
|
||||
expect($result)->toEqual('2026-06-01 01:42:49');
|
||||
});
|
||||
|
||||
test('set converts ISO 8601 string to datetime string', function (): void {
|
||||
$cast = new CarbonCast();
|
||||
$value = '2026-06-01T01:42:49+00:00';
|
||||
|
||||
$result = $cast->set(new Pirep(), 'block_on_time', $value, []);
|
||||
|
||||
expect($result)->toEqual('2026-06-01 01:42:49');
|
||||
});
|
||||
|
||||
test('set converts standard datetime string to datetime string', function (): void {
|
||||
$cast = new CarbonCast();
|
||||
$value = '2026-06-01 01:42:49';
|
||||
|
||||
$result = $cast->set(new Pirep(), 'block_on_time', $value, []);
|
||||
|
||||
expect($result)->toEqual('2026-06-01 01:42:49');
|
||||
});
|
||||
|
||||
test('set passes through null unchanged', function (): void {
|
||||
$cast = new CarbonCast();
|
||||
|
||||
$result = $cast->set(new Pirep(), 'block_on_time', null, []);
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
test('get converts string to Carbon instance', function (): void {
|
||||
$cast = new CarbonCast();
|
||||
|
||||
$result = $cast->get(new Pirep(), 'block_on_time', '2026-06-01 01:42:49', []);
|
||||
|
||||
expect($result)->toBeInstanceOf(Carbon::class);
|
||||
expect($result->toDateTimeString())->toEqual('2026-06-01 01:42:49');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user