Compare commits

...

5 Commits

Author SHA1 Message Date
Chris Smith
630323feed Add android app 2025-12-12 21:35:12 +01:00
Chris Smith
97f3a444ec add .gitignore 2025-12-12 21:34:29 +01:00
Chris Smith
2700955f31 Moving folders 2025-12-12 21:34:09 +01:00
Chris Smith
ae9dc9d805 ignore .idea folder 2025-12-12 21:23:13 +01:00
Chris Smith
d225d12555 Initial commit 2025-12-12 21:22:48 +01:00
67 changed files with 1902 additions and 3592 deletions

27
.env
View File

@@ -1,27 +0,0 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###
###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###

View File

@@ -1,4 +0,0 @@
###> symfony/framework-bundle ###
APP_SECRET=b9f97dbf63662a281f041195fe989f8e
###< symfony/framework-bundle ###

13
.gitignore vendored
View File

@@ -1,10 +1,3 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
.env
.idea
/api/db/database.sqlite

View File

@@ -1,3 +1,68 @@
# listthingy-api
# List Thingy
A list API based off of Symfony for shopping, todos, or other lists
A collaborative list management platform for shopping, todos, and other lists. Create lists instantly without signup, share with friends via UUID, and collaborate in real-time.
## Philosophy
- No accounts
- No ads
- No bloat
- Code that just works for 15 years
## Project Structure
This monorepo contains three components:
### [API](./api)
Pure PHP REST API with SQLite database. Handles all list and item operations with WebSocket support for real-time collaboration.
**Tech Stack:**
- Pure PHP 8.0+ (no frameworks)
- SQLite with PDO
- Custom PSR-4 autoloader
- .env configuration
[See API documentation →](./api/README.md)
## Quick Start
### API Server
```bash
cd api
cp .env.example .env
cd public
php -S localhost:8000
```
### Mobile Apps
Coming soon
## Features
- Create lists without signup
- Share lists via UUID
- Collaborative editing
- Real-time updates (WebSocket)
- Optimistic update and save
- Categorized items
- Quantity tracking
- Soft delete (items)
## API Endpoints
#### Lists
- POST /list
- GET /list/{uuid}
- PATCH /list/{uuid}
- DELETE /list/{uuid}
#### Items
- POST /list/{uuid}/item
- PATCH /list/{uuid}/item/{id}
- DELETE /list/{uuid}/item/{id}
## License
See [LICENSE](./LICENSE) file for details.

15
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

1
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,47 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.sometimescode.listthingy"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "com.sometimescode.listthingy"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.sometimescode.listthingy
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.sometimescode.listthingy", appContext.packageName)
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ListThingy" />
</manifest>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ListThingy" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">List Thingy</string>
</resources>

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ListThingy" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.sometimescode.listthingy
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

5
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}

23
android/gradle.properties Normal file
View File

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

@@ -0,0 +1,22 @@
[versions]
agp = "8.13.2"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

Binary file not shown.

View File

@@ -0,0 +1,8 @@
#Fri Dec 12 21:31:00 CET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "List Thingy"
include(":app")

10
api/.env.example Normal file
View File

@@ -0,0 +1,10 @@
# Database Configuration
DB_PATH=db/database.sqlite
# CORS Configuration
CORS_ALLOW_ORIGIN=*
CORS_ALLOW_METHODS="GET, POST, PATCH, DELETE, OPTIONS"
CORS_ALLOW_HEADERS=Content-Type
# Application Configuration
ERROR_REPORTING=true

161
api/README.md Normal file
View File

@@ -0,0 +1,161 @@
# list thingy API
A list thingy API using plain PHP for shopping, todos, or other lists. Users create a list instantly without any signup.
They will get a UUID and can share it with friends and collaborate on lists. This API will manage the list with API
calls and will also run a websocket the iOS and Android apps can connect to.
- No accounts
- No ads
- No bloat
- Code that just works for 15 years
## API
#### Lists
- POST /list
- GET /list/{uuid}
- PATCH /list/{uuid}
- DELETE /list/{uuid}
#### Items
- POST /list/{uuid}/item
- PATCH /list/{uuid}/item/{id}
- DELETE /list/{uuid}/item/{id}
## Setup
### Requirements
- PHP 8.0 or higher
- SQLite3 extension (usually included with PHP)
### Installation
1. Clone the repository
2. Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
3. (Optional) Edit `.env` to customize configuration
4. Start the PHP development server:
```bash
cd public
php -S localhost:8000
```
The database will be automatically created on first request at `db/database.sqlite`.
### Configuration
All configuration is managed through the `.env` file:
- `DB_PATH` - Path to SQLite database file (relative or absolute)
- `CORS_ALLOW_ORIGIN` - CORS allowed origins (default: `*`)
- `CORS_ALLOW_METHODS` - CORS allowed HTTP methods
- `CORS_ALLOW_HEADERS` - CORS allowed headers
- `ERROR_REPORTING` - Enable/disable error reporting (true/false)
## Usage Examples
### Create a List
```bash
curl -X POST http://localhost:8000/list \
-H "Content-Type: application/json" \
-d '{"name":"Shopping List","sharable":true}'
```
Response:
```json
{
"success": true,
"data": {
"id": 1,
"uuid": "a1b2c3d4e5f6...",
"name": "Shopping List",
"sharable": true,
"created_at": "2025-12-12 12:00:00"
}
}
```
### Get a List with Items
```bash
curl http://localhost:8000/list/{uuid}
```
### Update a List
```bash
curl -X PATCH http://localhost:8000/list/{uuid} \
-H "Content-Type: application/json" \
-d '{"name":"Updated List Name"}'
```
### Delete a List
```bash
curl -X DELETE http://localhost:8000/list/{uuid}
```
### Add Item to List
```bash
curl -X POST http://localhost:8000/list/{uuid}/item \
-H "Content-Type: application/json" \
-d '{"name":"Milk","quantity":2.5,"category":"Dairy"}'
```
### Update Item
```bash
curl -X PATCH http://localhost:8000/list/{uuid}/item/{id} \
-H "Content-Type: application/json" \
-d '{"quantity":3}'
```
### Delete Item (Soft Delete)
```bash
curl -X DELETE http://localhost:8000/list/{uuid}/item/{id}
```
## Models
#### List Model
- id
- uuid
- name
- sharable (boolean)
- created_at
#### Item Model
- id
- list_id
- category
- quantity (double)
- name
- created_at
- deleted_at
## Architecture
Pure PHP with no dependencies:
- **Database**: SQLite with PDO
- **Router**: Custom lightweight router
- **Structure**: Simple MVC pattern
- **CORS**: Enabled for mobile/web apps
## WebSocket Server
WebSocket support will be implemented separately for real-time collaboration features.

30
api/config/config.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/Env.php';
use App\Env;
$envPath = __DIR__ . '/../.env';
if (file_exists($envPath)) {
Env::load($envPath);
}
$dbPath = Env::get('DB_PATH', 'db/database.sqlite');
if (!str_starts_with($dbPath, '/')) {
$dbPath = __DIR__ . '/../' . $dbPath;
}
return [
'db' => [
'path' => $dbPath,
],
'cors' => [
'allow_origin' => Env::get('CORS_ALLOW_ORIGIN', '*'),
'allow_methods' => Env::get('CORS_ALLOW_METHODS', 'GET, POST, PATCH, DELETE, OPTIONS'),
'allow_headers' => Env::get('CORS_ALLOW_HEADERS', 'Content-Type'),
],
'error_reporting' => Env::getBool('ERROR_REPORTING', true),
];

66
api/public/index.php Normal file
View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/Autoloader.php';
$autoloader = new Autoloader('App', __DIR__ . '/../src');
$autoloader->register();
$config = require __DIR__ . '/../config/config.php';
if ($config['error_reporting']) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
}
header('Access-Control-Allow-Origin: ' . $config['cors']['allow_origin']);
header('Access-Control-Allow-Methods: ' . $config['cors']['allow_methods']);
header('Access-Control-Allow-Headers: ' . $config['cors']['allow_headers']);
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
use App\Database;
use App\Router;
use App\Controllers\ListController;
use App\Controllers\ItemController;
Database::init($config['db']['path']);
$router = new Router();
$router->addRoute('POST', '/list', function () {
ListController::create();
});
$router->addRoute('GET', '/list/{uuid}', function (string $uuid) {
ListController::show($uuid);
});
$router->addRoute('PATCH', '/list/{uuid}', function (string $uuid) {
ListController::update($uuid);
});
$router->addRoute('DELETE', '/list/{uuid}', function (string $uuid) {
ListController::delete($uuid);
});
$router->addRoute('POST', '/list/{uuid}/item', function (string $uuid) {
ItemController::create($uuid);
});
$router->addRoute('PATCH', '/list/{uuid}/item/{id}', function (string $uuid, string $id) {
ItemController::update($uuid, $id);
});
$router->addRoute('DELETE', '/list/{uuid}/item/{id}', function (string $uuid, string $id) {
ItemController::delete($uuid, $id);
});
try {
$router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
} catch (Exception $e) {
Router::sendResponse(['error' => 'Internal server error'], 500);
}

33
api/src/Autoloader.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
class Autoloader
{
private string $baseDir;
private string $namespace;
public function __construct(string $namespace, string $baseDir)
{
$this->namespace = rtrim($namespace, '\\') . '\\';
$this->baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
public function register(): void
{
spl_autoload_register([$this, 'loadClass']);
}
private function loadClass(string $class): void
{
if (strpos($class, $this->namespace) !== 0) {
return;
}
$relativeClass = substr($class, strlen($this->namespace));
$file = $this->baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Models\ListModel;
use App\Models\ItemModel;
use App\Router;
use Exception;
class ItemController
{
public static function create(string $listUuid): void
{
$input = Router::getJsonInput();
if (empty($input['name'])) {
Router::sendResponse(['error' => 'Name is required'], 400);
}
try {
$list = ListModel::findByUuid($listUuid);
if (!$list) {
Router::sendResponse(['error' => 'List not found'], 404);
}
$item = ItemModel::create($list['id'], $input);
Router::sendResponse($item, 201);
} catch (Exception $e) {
Router::sendResponse(['error' => 'Failed to create item'], 500);
}
}
public static function update(string $listUuid, string $itemId): void
{
$input = Router::getJsonInput();
if (empty($input)) {
Router::sendResponse(['error' => 'No data provided'], 400);
}
try {
$list = ListModel::findByUuid($listUuid);
if (!$list) {
Router::sendResponse(['error' => 'List not found'], 404);
}
$item = ItemModel::update($list['id'], (int) $itemId, $input);
if (!$item) {
Router::sendResponse(['error' => 'Item not found'], 404);
}
Router::sendResponse($item, 200);
} catch (Exception $e) {
Router::sendResponse(['error' => 'Failed to update item'], 500);
}
}
public static function delete(string $listUuid, string $itemId): void
{
try {
$list = ListModel::findByUuid($listUuid);
if (!$list) {
Router::sendResponse(['error' => 'List not found'], 404);
}
$deleted = ItemModel::delete($list['id'], (int) $itemId);
if (!$deleted) {
Router::sendResponse(['error' => 'Item not found'], 404);
}
http_response_code(204);
exit;
} catch (Exception $e) {
Router::sendResponse(['error' => 'Failed to delete item'], 500);
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Models\ListModel;
use App\Models\ItemModel;
use App\Router;
use Exception;
class ListController
{
public static function create(): void
{
$input = Router::getJsonInput();
if (empty($input['name'])) {
Router::sendResponse(['error' => 'Name is required'], 400);
}
$sharable = $input['sharable'] ?? true;
try {
$list = ListModel::create($input['name'], $sharable);
Router::sendResponse($list, 201);
} catch (Exception $e) {
Router::sendResponse(['error' => 'Failed to create list'], 500);
}
}
public static function show(string $uuid): void
{
try {
$list = ListModel::findByUuid($uuid);
if (!$list) {
Router::sendResponse(['error' => 'List not found'], 404);
}
$items = ItemModel::findByListId($list['id']);
$list['items'] = $items;
Router::sendResponse($list, 200);
} catch (Exception $e) {
Router::sendResponse(['error' => 'Failed to retrieve list'], 500);
}
}
public static function update(string $uuid): void
{
$input = Router::getJsonInput();
if (empty($input)) {
Router::sendResponse(['error' => 'No data provided'], 400);
}
try {
$list = ListModel::update($uuid, $input);
if (!$list) {
Router::sendResponse(['error' => 'List not found'], 404);
}
Router::sendResponse($list, 200);
} catch (Exception $e) {
Router::sendResponse(['error' => 'Failed to update list'], 500);
}
}
public static function delete(string $uuid): void
{
try {
$deleted = ListModel::delete($uuid);
if (!$deleted) {
Router::sendResponse(['error' => 'List not found'], 404);
}
http_response_code(204);
exit;
} catch (Exception $e) {
Router::sendResponse(['error' => 'Failed to delete list'], 500);
}
}
}

168
api/src/Database.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace App;
use PDO;
class Database
{
private static ?PDO $connection = null;
private static string $dbPath;
public static function init(string $dbPath): void
{
self::$dbPath = $dbPath;
}
public static function getConnection(): PDO
{
if (self::$connection === null) {
$dbExists = file_exists(self::$dbPath);
self::$connection = new PDO('sqlite:' . self::$dbPath);
self::$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$connection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
if (!$dbExists) {
self::createSchema();
}
}
return self::$connection;
}
private static function createSchema(): void
{
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
sharable INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
list_id INTEGER NOT NULL,
category TEXT,
quantity REAL DEFAULT 1.0,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
FOREIGN KEY (list_id) REFERENCES lists(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_items_list_id ON items(list_id);
CREATE INDEX IF NOT EXISTS idx_items_deleted_at ON items(deleted_at);
SQL;
self::$connection->exec($sql);
}
public static function select(string $table, array $where = []): array
{
$db = self::getConnection();
$sql = "SELECT * FROM {$table}";
if (!empty($where)) {
$conditions = [];
foreach (array_keys($where) as $key) {
$conditions[] = "{$key} = :{$key}";
}
$sql .= ' WHERE ' . implode(' AND ', $conditions);
}
$stmt = $db->prepare($sql);
$stmt->execute($where);
return $stmt->fetchAll();
}
public static function selectOne(string $table, array $where): ?array
{
$results = self::select($table, $where);
return $results[0] ?? null;
}
public static function insert(string $table, array $data): int
{
$db = self::getConnection();
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$stmt = $db->prepare($sql);
$stmt->execute($data);
return (int) $db->lastInsertId();
}
public static function update(string $table, array $data, array $where): int
{
$db = self::getConnection();
$setParts = [];
foreach (array_keys($data) as $key) {
$setParts[] = "{$key} = :set_{$key}";
}
$whereParts = [];
foreach (array_keys($where) as $key) {
$whereParts[] = "{$key} = :where_{$key}";
}
$sql = "UPDATE {$table} SET " . implode(', ', $setParts) . ' WHERE ' . implode(' AND ', $whereParts);
$params = [];
foreach ($data as $key => $value) {
$params["set_{$key}"] = $value;
}
foreach ($where as $key => $value) {
$params["where_{$key}"] = $value;
}
$stmt = $db->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
public static function delete(string $table, array $where): int
{
$db = self::getConnection();
$conditions = [];
foreach (array_keys($where) as $key) {
$conditions[] = "{$key} = :{$key}";
}
$sql = "DELETE FROM {$table} WHERE " . implode(' AND ', $conditions);
$stmt = $db->prepare($sql);
$stmt->execute($where);
return $stmt->rowCount();
}
public static function query(string $sql, array $params = []): array
{
$db = self::getConnection();
$stmt = $db->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public static function execute(string $sql, array $params = []): int
{
$db = self::getConnection();
$stmt = $db->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
}

107
api/src/Env.php Normal file
View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace App;
class Env
{
private static array $variables = [];
private static bool $loaded = false;
public static function load(string $path): void
{
if (self::$loaded) {
return;
}
if (!file_exists($path)) {
throw new \RuntimeException("Environment file not found: {$path}");
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line) || str_starts_with($line, '#')) {
continue;
}
if (!str_contains($line, '=')) {
continue;
}
[$name, $value] = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
$value = self::parseValue($value);
self::$variables[$name] = $value;
$_ENV[$name] = $value;
if (!array_key_exists($name, $_SERVER)) {
$_SERVER[$name] = $value;
}
}
self::$loaded = true;
}
private static function parseValue(string $value): string
{
if (preg_match('/^"(.*)"$/', $value, $matches)) {
return $matches[1];
}
if (preg_match('/^\'(.*)\'$/', $value, $matches)) {
return $matches[1];
}
return $value;
}
public static function get(string $name, ?string $default = null): ?string
{
if (isset(self::$variables[$name])) {
return self::$variables[$name];
}
if (isset($_ENV[$name])) {
return $_ENV[$name];
}
if (isset($_SERVER[$name])) {
return $_SERVER[$name];
}
return $default;
}
public static function require(string $name): string
{
$value = self::get($name);
if ($value === null) {
throw new \RuntimeException("Required environment variable '{$name}' is not set");
}
return $value;
}
public static function has(string $name): bool
{
return self::get($name) !== null;
}
public static function getBool(string $name, bool $default = false): bool
{
$value = self::get($name);
if ($value === null) {
return $default;
}
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Database;
class ItemModel
{
public static function create(int $listId, array $data): array
{
$insertData = [
'list_id' => $listId,
'name' => $data['name'] ?? '',
'category' => $data['category'] ?? null,
'quantity' => $data['quantity'] ?? 1.0,
];
$id = Database::insert('items', $insertData);
return self::findById($id);
}
public static function findById(int $id): ?array
{
$item = Database::selectOne('items', ['id' => $id]);
if ($item) {
$item['quantity'] = (float) $item['quantity'];
}
return $item;
}
public static function findByListId(int $listId): array
{
$items = Database::query(
'SELECT * FROM items WHERE list_id = :list_id AND deleted_at IS NULL ORDER BY created_at DESC',
['list_id' => $listId]
);
foreach ($items as &$item) {
$item['quantity'] = (float) $item['quantity'];
}
return $items;
}
public static function update(int $listId, int $itemId, array $data): ?array
{
$item = Database::selectOne('items', ['id' => $itemId, 'list_id' => $listId]);
if (!$item) {
return null;
}
$updateData = [];
if (isset($data['name'])) {
$updateData['name'] = $data['name'];
}
if (isset($data['category'])) {
$updateData['category'] = $data['category'];
}
if (isset($data['quantity'])) {
$updateData['quantity'] = $data['quantity'];
}
if (empty($updateData)) {
return self::findById($itemId);
}
Database::update('items', $updateData, ['id' => $itemId, 'list_id' => $listId]);
return self::findById($itemId);
}
public static function delete(int $listId, int $itemId): bool
{
$updated = Database::execute(
'UPDATE items SET deleted_at = CURRENT_TIMESTAMP WHERE id = :id AND list_id = :list_id AND deleted_at IS NULL',
['id' => $itemId, 'list_id' => $listId]
);
return $updated > 0;
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Database;
class ListModel
{
public static function create(string $name, bool $sharable = false): array
{
$uuid = bin2hex(random_bytes(16));
$id = Database::insert('lists', [
'uuid' => $uuid,
'name' => $name,
'sharable' => $sharable ? 1 : 0,
]);
return self::findById($id);
}
public static function findById(int $id): ?array
{
$list = Database::selectOne('lists', ['id' => $id]);
if ($list) {
$list['sharable'] = (bool) $list['sharable'];
}
return $list;
}
public static function findByUuid(string $uuid): ?array
{
$list = Database::selectOne('lists', ['uuid' => $uuid]);
if ($list) {
$list['sharable'] = (bool) $list['sharable'];
}
return $list;
}
public static function update(string $uuid, array $data): ?array
{
$list = self::findByUuid($uuid);
if (!$list) {
return null;
}
$updateData = [];
if (isset($data['name'])) {
$updateData['name'] = $data['name'];
}
if (isset($data['sharable'])) {
$updateData['sharable'] = $data['sharable'] ? 1 : 0;
}
if (empty($updateData)) {
return $list;
}
Database::update('lists', $updateData, ['uuid' => $uuid]);
return self::findByUuid($uuid);
}
public static function delete(string $uuid): bool
{
$deleted = Database::delete('lists', ['uuid' => $uuid]);
return $deleted > 0;
}
}

68
api/src/Router.php Normal file
View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App;
class Router
{
private array $routes = [];
public function addRoute(string $method, string $path, callable $handler): void
{
$this->routes[] = [
'method' => strtoupper($method),
'path' => $path,
'handler' => $handler,
];
}
public function dispatch(string $method, string $uri): void
{
$method = strtoupper($method);
$uri = parse_url($uri, PHP_URL_PATH);
$uri = rtrim($uri, '/') ?: '/';
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
$pattern = $this->convertPathToRegex($route['path']);
if (preg_match($pattern, $uri, $matches)) {
array_shift($matches);
call_user_func_array($route['handler'], $matches);
return;
}
}
$this->sendResponse(['error' => 'Not Found'], 404);
}
private function convertPathToRegex(string $path): string
{
$path = rtrim($path, '/') ?: '/';
$pattern = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', '([^/]+)', $path);
return '#^' . $pattern . '$#';
}
public static function getJsonInput(): array
{
$input = file_get_contents('php://input');
return json_decode($input, true) ?? [];
}
public static function sendResponse(array $data, int $statusCode = 200): void
{
http_response_code($statusCode);
header('Content-Type: application/json');
if ($statusCode >= 400) {
echo json_encode(['success' => false, 'error' => $data['error'] ?? 'An error occurred']);
} else {
echo json_encode(['success' => true, 'data' => $data]);
}
exit;
}
}

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

View File

@@ -1,71 +0,0 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"symfony/console": "8.0.*",
"symfony/dotenv": "8.0.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "8.0.*",
"symfony/runtime": "8.0.*",
"symfony/yaml": "8.0.*"
},
"require-dev": {
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"bump-after-update": true,
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*",
"symfony/polyfill-php83": "*",
"symfony/polyfill-php84": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "8.0.*"
}
}
}

2482
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
];

View File

@@ -1,19 +0,0 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

View File

@@ -1,15 +0,0 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View File

@@ -1,10 +0,0 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%'
when@prod:
framework:
router:
strict_requirements: null

View File

@@ -1,5 +0,0 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

View File

@@ -1,803 +0,0 @@
<?php
// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
/**
* This class provides array-shapes for configuring the services and bundles of an application.
*
* Services declared with the config() method below are autowired and autoconfigured by default.
*
* This is for apps only. Bundles SHOULD NOT use it.
*
* Example:
*
* ```php
* // config/services.php
* namespace Symfony\Component\DependencyInjection\Loader\Configurator;
*
* return App::config([
* 'services' => [
* 'App\\' => [
* 'resource' => '../src/',
* ],
* ],
* ]);
* ```
*
* @psalm-type ImportsConfig = list<string|array{
* resource: string,
* type?: string|null,
* ignore_errors?: bool,
* }>
* @psalm-type ParametersConfig = array<string, scalar|\UnitEnum|array<scalar|\UnitEnum|array<mixed>|null>|null>
* @psalm-type ArgumentsType = list<mixed>|array<string, mixed>
* @psalm-type CallType = array<string, ArgumentsType>|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool}
* @psalm-type TagsType = list<string|array<string, array<string, mixed>>> // arrays inside the list must have only one element, with the tag name as the key
* @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator|ExpressionConfigurator
* @psalm-type DeprecationType = array{package: string, version: string, message?: string}
* @psalm-type DefaultsType = array{
* public?: bool,
* tags?: TagsType,
* resource_tags?: TagsType,
* autowire?: bool,
* autoconfigure?: bool,
* bind?: array<string, mixed>,
* }
* @psalm-type InstanceofType = array{
* shared?: bool,
* lazy?: bool|string,
* public?: bool,
* properties?: array<string, mixed>,
* configurator?: CallbackType,
* calls?: list<CallType>,
* tags?: TagsType,
* resource_tags?: TagsType,
* autowire?: bool,
* bind?: array<string, mixed>,
* constructor?: string,
* }
* @psalm-type DefinitionType = array{
* class?: string,
* file?: string,
* parent?: string,
* shared?: bool,
* synthetic?: bool,
* lazy?: bool|string,
* public?: bool,
* abstract?: bool,
* deprecated?: DeprecationType,
* factory?: CallbackType,
* configurator?: CallbackType,
* arguments?: ArgumentsType,
* properties?: array<string, mixed>,
* calls?: list<CallType>,
* tags?: TagsType,
* resource_tags?: TagsType,
* decorates?: string,
* decoration_inner_name?: string,
* decoration_priority?: int,
* decoration_on_invalid?: 'exception'|'ignore'|null,
* autowire?: bool,
* autoconfigure?: bool,
* bind?: array<string, mixed>,
* constructor?: string,
* from_callable?: CallbackType,
* }
* @psalm-type AliasType = string|array{
* alias: string,
* public?: bool,
* deprecated?: DeprecationType,
* }
* @psalm-type PrototypeType = array{
* resource: string,
* namespace?: string,
* exclude?: string|list<string>,
* parent?: string,
* shared?: bool,
* lazy?: bool|string,
* public?: bool,
* abstract?: bool,
* deprecated?: DeprecationType,
* factory?: CallbackType,
* arguments?: ArgumentsType,
* properties?: array<string, mixed>,
* configurator?: CallbackType,
* calls?: list<CallType>,
* tags?: TagsType,
* resource_tags?: TagsType,
* autowire?: bool,
* autoconfigure?: bool,
* bind?: array<string, mixed>,
* constructor?: string,
* }
* @psalm-type StackType = array{
* stack: list<DefinitionType|AliasType|PrototypeType|array<class-string, ArgumentsType|null>>,
* public?: bool,
* deprecated?: DeprecationType,
* }
* @psalm-type ServicesConfig = array{
* _defaults?: DefaultsType,
* _instanceof?: InstanceofType,
* ...<string, DefinitionType|AliasType|PrototypeType|StackType|ArgumentsType|null>
* }
* @psalm-type ExtensionType = array<string, mixed>
* @psalm-type FrameworkConfig = array{
* secret?: scalar|null,
* http_method_override?: bool, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false
* allowed_http_method_override?: list<string>|null,
* trust_x_sendfile_type_header?: scalar|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%"
* ide?: scalar|null, // Default: "%env(default::SYMFONY_IDE)%"
* test?: bool,
* default_locale?: scalar|null, // Default: "en"
* set_locale_from_accept_language?: bool, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false
* set_content_language_from_locale?: bool, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false
* enabled_locales?: list<scalar|null>,
* trusted_hosts?: list<scalar|null>,
* trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"]
* trusted_headers?: list<scalar|null>,
* error_controller?: scalar|null, // Default: "error_controller"
* handle_all_throwables?: bool, // HttpKernel will handle all kinds of \Throwable. // Default: true
* csrf_protection?: bool|array{
* enabled?: scalar|null, // Default: null
* stateless_token_ids?: list<scalar|null>,
* check_header?: scalar|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false
* cookie_name?: scalar|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token"
* },
* form?: bool|array{ // Form configuration
* enabled?: bool, // Default: false
* csrf_protection?: array{
* enabled?: scalar|null, // Default: null
* token_id?: scalar|null, // Default: null
* field_name?: scalar|null, // Default: "_token"
* field_attr?: array<string, scalar|null>,
* },
* },
* http_cache?: bool|array{ // HTTP cache configuration
* enabled?: bool, // Default: false
* debug?: bool, // Default: "%kernel.debug%"
* trace_level?: "none"|"short"|"full",
* trace_header?: scalar|null,
* default_ttl?: int,
* private_headers?: list<scalar|null>,
* skip_response_headers?: list<scalar|null>,
* allow_reload?: bool,
* allow_revalidate?: bool,
* stale_while_revalidate?: int,
* stale_if_error?: int,
* terminate_on_cache_hit?: bool,
* },
* esi?: bool|array{ // ESI configuration
* enabled?: bool, // Default: false
* },
* ssi?: bool|array{ // SSI configuration
* enabled?: bool, // Default: false
* },
* fragments?: bool|array{ // Fragments configuration
* enabled?: bool, // Default: false
* hinclude_default_template?: scalar|null, // Default: null
* path?: scalar|null, // Default: "/_fragment"
* },
* profiler?: bool|array{ // Profiler configuration
* enabled?: bool, // Default: false
* collect?: bool, // Default: true
* collect_parameter?: scalar|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null
* only_exceptions?: bool, // Default: false
* only_main_requests?: bool, // Default: false
* dsn?: scalar|null, // Default: "file:%kernel.cache_dir%/profiler"
* collect_serializer_data?: true, // Default: true
* },
* workflows?: bool|array{
* enabled?: bool, // Default: false
* workflows?: array<string, array{ // Default: []
* audit_trail?: bool|array{
* enabled?: bool, // Default: false
* },
* type?: "workflow"|"state_machine", // Default: "state_machine"
* marking_store?: array{
* type?: "method",
* property?: scalar|null,
* service?: scalar|null,
* },
* supports?: list<scalar|null>,
* definition_validators?: list<scalar|null>,
* support_strategy?: scalar|null,
* initial_marking?: list<scalar|null>,
* events_to_dispatch?: list<string>|null,
* places?: list<array{ // Default: []
* name: scalar|null,
* metadata?: list<mixed>,
* }>,
* transitions: list<array{ // Default: []
* name: string,
* guard?: string, // An expression to block the transition.
* from?: list<array{ // Default: []
* place: string,
* weight?: int, // Default: 1
* }>,
* to?: list<array{ // Default: []
* place: string,
* weight?: int, // Default: 1
* }>,
* weight?: int, // Default: 1
* metadata?: list<mixed>,
* }>,
* metadata?: list<mixed>,
* }>,
* },
* router?: bool|array{ // Router configuration
* enabled?: bool, // Default: false
* resource: scalar|null,
* type?: scalar|null,
* default_uri?: scalar|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null
* http_port?: scalar|null, // Default: 80
* https_port?: scalar|null, // Default: 443
* strict_requirements?: scalar|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true
* utf8?: bool, // Default: true
* },
* session?: bool|array{ // Session configuration
* enabled?: bool, // Default: false
* storage_factory_id?: scalar|null, // Default: "session.storage.factory.native"
* handler_id?: scalar|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null.
* name?: scalar|null,
* cookie_lifetime?: scalar|null,
* cookie_path?: scalar|null,
* cookie_domain?: scalar|null,
* cookie_secure?: true|false|"auto", // Default: "auto"
* cookie_httponly?: bool, // Default: true
* cookie_samesite?: null|"lax"|"strict"|"none", // Default: "lax"
* use_cookies?: bool,
* gc_divisor?: scalar|null,
* gc_probability?: scalar|null,
* gc_maxlifetime?: scalar|null,
* save_path?: scalar|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null.
* metadata_update_threshold?: int, // Seconds to wait between 2 session metadata updates. // Default: 0
* },
* request?: bool|array{ // Request configuration
* enabled?: bool, // Default: false
* formats?: array<string, string|list<scalar|null>>,
* },
* assets?: bool|array{ // Assets configuration
* enabled?: bool, // Default: false
* strict_mode?: bool, // Throw an exception if an entry is missing from the manifest.json. // Default: false
* version_strategy?: scalar|null, // Default: null
* version?: scalar|null, // Default: null
* version_format?: scalar|null, // Default: "%%s?%%s"
* json_manifest_path?: scalar|null, // Default: null
* base_path?: scalar|null, // Default: ""
* base_urls?: list<scalar|null>,
* packages?: array<string, array{ // Default: []
* strict_mode?: bool, // Throw an exception if an entry is missing from the manifest.json. // Default: false
* version_strategy?: scalar|null, // Default: null
* version?: scalar|null,
* version_format?: scalar|null, // Default: null
* json_manifest_path?: scalar|null, // Default: null
* base_path?: scalar|null, // Default: ""
* base_urls?: list<scalar|null>,
* }>,
* },
* asset_mapper?: bool|array{ // Asset Mapper configuration
* enabled?: bool, // Default: false
* paths?: array<string, scalar|null>,
* excluded_patterns?: list<scalar|null>,
* exclude_dotfiles?: bool, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true
* server?: bool, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true
* public_prefix?: scalar|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/"
* missing_import_mode?: "strict"|"warn"|"ignore", // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn"
* extensions?: array<string, scalar|null>,
* importmap_path?: scalar|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php"
* importmap_polyfill?: scalar|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims"
* importmap_script_attributes?: array<string, scalar|null>,
* vendor_dir?: scalar|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor"
* precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip.
* enabled?: bool, // Default: false
* formats?: list<scalar|null>,
* extensions?: list<scalar|null>,
* },
* },
* translator?: bool|array{ // Translator configuration
* enabled?: bool, // Default: false
* fallbacks?: list<scalar|null>,
* logging?: bool, // Default: false
* formatter?: scalar|null, // Default: "translator.formatter.default"
* cache_dir?: scalar|null, // Default: "%kernel.cache_dir%/translations"
* default_path?: scalar|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations"
* paths?: list<scalar|null>,
* pseudo_localization?: bool|array{
* enabled?: bool, // Default: false
* accents?: bool, // Default: true
* expansion_factor?: float, // Default: 1.0
* brackets?: bool, // Default: true
* parse_html?: bool, // Default: false
* localizable_html_attributes?: list<scalar|null>,
* },
* providers?: array<string, array{ // Default: []
* dsn?: scalar|null,
* domains?: list<scalar|null>,
* locales?: list<scalar|null>,
* }>,
* globals?: array<string, string|array{ // Default: []
* value?: mixed,
* message?: string,
* parameters?: array<string, scalar|null>,
* domain?: string,
* }>,
* },
* validation?: bool|array{ // Validation configuration
* enabled?: bool, // Default: false
* enable_attributes?: bool, // Default: true
* static_method?: list<scalar|null>,
* translation_domain?: scalar|null, // Default: "validators"
* email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict", // Default: "html5"
* mapping?: array{
* paths?: list<scalar|null>,
* },
* not_compromised_password?: bool|array{
* enabled?: bool, // When disabled, compromised passwords will be accepted as valid. // Default: true
* endpoint?: scalar|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null
* },
* disable_translation?: bool, // Default: false
* auto_mapping?: array<string, array{ // Default: []
* services?: list<scalar|null>,
* }>,
* },
* serializer?: bool|array{ // Serializer configuration
* enabled?: bool, // Default: false
* enable_attributes?: bool, // Default: true
* name_converter?: scalar|null,
* circular_reference_handler?: scalar|null,
* max_depth_handler?: scalar|null,
* mapping?: array{
* paths?: list<scalar|null>,
* },
* default_context?: list<mixed>,
* named_serializers?: array<string, array{ // Default: []
* name_converter?: scalar|null,
* default_context?: list<mixed>,
* include_built_in_normalizers?: bool, // Whether to include the built-in normalizers // Default: true
* include_built_in_encoders?: bool, // Whether to include the built-in encoders // Default: true
* }>,
* },
* property_access?: bool|array{ // Property access configuration
* enabled?: bool, // Default: false
* magic_call?: bool, // Default: false
* magic_get?: bool, // Default: true
* magic_set?: bool, // Default: true
* throw_exception_on_invalid_index?: bool, // Default: false
* throw_exception_on_invalid_property_path?: bool, // Default: true
* },
* type_info?: bool|array{ // Type info configuration
* enabled?: bool, // Default: false
* aliases?: array<string, scalar|null>,
* },
* property_info?: bool|array{ // Property info configuration
* enabled?: bool, // Default: false
* with_constructor_extractor?: bool, // Registers the constructor extractor. // Default: true
* },
* cache?: array{ // Cache configuration
* prefix_seed?: scalar|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%"
* app?: scalar|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem"
* system?: scalar|null, // System related cache pools configuration. // Default: "cache.adapter.system"
* directory?: scalar|null, // Default: "%kernel.share_dir%/pools/app"
* default_psr6_provider?: scalar|null,
* default_redis_provider?: scalar|null, // Default: "redis://localhost"
* default_valkey_provider?: scalar|null, // Default: "valkey://localhost"
* default_memcached_provider?: scalar|null, // Default: "memcached://localhost"
* default_doctrine_dbal_provider?: scalar|null, // Default: "database_connection"
* default_pdo_provider?: scalar|null, // Default: null
* pools?: array<string, array{ // Default: []
* adapters?: list<scalar|null>,
* tags?: scalar|null, // Default: null
* public?: bool, // Default: false
* default_lifetime?: scalar|null, // Default lifetime of the pool.
* provider?: scalar|null, // Overwrite the setting from the default provider for this adapter.
* early_expiration_message_bus?: scalar|null,
* clearer?: scalar|null,
* }>,
* },
* php_errors?: array{ // PHP errors handling configuration
* log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true
* throw?: bool, // Throw PHP errors as \ErrorException instances. // Default: true
* },
* exceptions?: array<string, array{ // Default: []
* log_level?: scalar|null, // The level of log message. Null to let Symfony decide. // Default: null
* status_code?: scalar|null, // The status code of the response. Null or 0 to let Symfony decide. // Default: null
* log_channel?: scalar|null, // The channel of log message. Null to let Symfony decide. // Default: null
* }>,
* web_link?: bool|array{ // Web links configuration
* enabled?: bool, // Default: false
* },
* lock?: bool|string|array{ // Lock configuration
* enabled?: bool, // Default: false
* resources?: array<string, string|list<scalar|null>>,
* },
* semaphore?: bool|string|array{ // Semaphore configuration
* enabled?: bool, // Default: false
* resources?: array<string, scalar|null>,
* },
* messenger?: bool|array{ // Messenger configuration
* enabled?: bool, // Default: false
* routing?: array<string, array{ // Default: []
* senders?: list<scalar|null>,
* }>,
* serializer?: array{
* default_serializer?: scalar|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer"
* symfony_serializer?: array{
* format?: scalar|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json"
* context?: array<string, mixed>,
* },
* },
* transports?: array<string, string|array{ // Default: []
* dsn?: scalar|null,
* serializer?: scalar|null, // Service id of a custom serializer to use. // Default: null
* options?: list<mixed>,
* failure_transport?: scalar|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
* retry_strategy?: string|array{
* service?: scalar|null, // Service id to override the retry strategy entirely. // Default: null
* max_retries?: int, // Default: 3
* delay?: int, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
* multiplier?: float, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2
* max_delay?: int, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
* jitter?: float, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1
* },
* rate_limiter?: scalar|null, // Rate limiter name to use when processing messages. // Default: null
* }>,
* failure_transport?: scalar|null, // Transport name to send failed messages to (after all retries have failed). // Default: null
* stop_worker_on_signals?: list<scalar|null>,
* default_bus?: scalar|null, // Default: null
* buses?: array<string, array{ // Default: {"messenger.bus.default":{"default_middleware":{"enabled":true,"allow_no_handlers":false,"allow_no_senders":true},"middleware":[]}}
* default_middleware?: bool|string|array{
* enabled?: bool, // Default: true
* allow_no_handlers?: bool, // Default: false
* allow_no_senders?: bool, // Default: true
* },
* middleware?: list<string|array{ // Default: []
* id: scalar|null,
* arguments?: list<mixed>,
* }>,
* }>,
* },
* scheduler?: bool|array{ // Scheduler configuration
* enabled?: bool, // Default: false
* },
* disallow_search_engine_index?: bool, // Enabled by default when debug is enabled. // Default: true
* http_client?: bool|array{ // HTTP Client configuration
* enabled?: bool, // Default: false
* max_host_connections?: int, // The maximum number of connections to a single host.
* default_options?: array{
* headers?: array<string, mixed>,
* vars?: array<string, mixed>,
* max_redirects?: int, // The maximum number of redirects to follow.
* http_version?: scalar|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.
* resolve?: array<string, scalar|null>,
* proxy?: scalar|null, // The URL of the proxy to pass requests through or null for automatic detection.
* no_proxy?: scalar|null, // A comma separated list of hosts that do not require a proxy to be reached.
* timeout?: float, // The idle timeout, defaults to the "default_socket_timeout" ini parameter.
* max_duration?: float, // The maximum execution time for the request+response as a whole.
* bindto?: scalar|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.
* verify_peer?: bool, // Indicates if the peer should be verified in a TLS context.
* verify_host?: bool, // Indicates if the host should exist as a certificate common name.
* cafile?: scalar|null, // A certificate authority file.
* capath?: scalar|null, // A directory that contains multiple certificate authority files.
* local_cert?: scalar|null, // A PEM formatted certificate file.
* local_pk?: scalar|null, // A private key file.
* passphrase?: scalar|null, // The passphrase used to encrypt the "local_pk" file.
* ciphers?: scalar|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)
* peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).
* sha1?: mixed,
* pin-sha256?: mixed,
* md5?: mixed,
* },
* crypto_method?: scalar|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.
* extra?: array<string, mixed>,
* rate_limiter?: scalar|null, // Rate limiter name to use for throttling requests. // Default: null
* caching?: bool|array{ // Caching configuration.
* enabled?: bool, // Default: false
* cache_pool?: string, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client"
* shared?: bool, // Indicates whether the cache is shared (public) or private. // Default: true
* max_ttl?: int, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null
* },
* retry_failed?: bool|array{
* enabled?: bool, // Default: false
* retry_strategy?: scalar|null, // service id to override the retry strategy. // Default: null
* http_codes?: array<string, array{ // Default: []
* code?: int,
* methods?: list<string>,
* }>,
* max_retries?: int, // Default: 3
* delay?: int, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
* multiplier?: float, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2
* max_delay?: int, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
* jitter?: float, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1
* },
* },
* mock_response_factory?: scalar|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable.
* scoped_clients?: array<string, string|array{ // Default: []
* scope?: scalar|null, // The regular expression that the request URL must match before adding the other options. When none is provided, the base URI is used instead.
* base_uri?: scalar|null, // The URI to resolve relative URLs, following rules in RFC 3985, section 2.
* auth_basic?: scalar|null, // An HTTP Basic authentication "username:password".
* auth_bearer?: scalar|null, // A token enabling HTTP Bearer authorization.
* auth_ntlm?: scalar|null, // A "username:password" pair to use Microsoft NTLM authentication (requires the cURL extension).
* query?: array<string, scalar|null>,
* headers?: array<string, mixed>,
* max_redirects?: int, // The maximum number of redirects to follow.
* http_version?: scalar|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version.
* resolve?: array<string, scalar|null>,
* proxy?: scalar|null, // The URL of the proxy to pass requests through or null for automatic detection.
* no_proxy?: scalar|null, // A comma separated list of hosts that do not require a proxy to be reached.
* timeout?: float, // The idle timeout, defaults to the "default_socket_timeout" ini parameter.
* max_duration?: float, // The maximum execution time for the request+response as a whole.
* bindto?: scalar|null, // A network interface name, IP address, a host name or a UNIX socket to bind to.
* verify_peer?: bool, // Indicates if the peer should be verified in a TLS context.
* verify_host?: bool, // Indicates if the host should exist as a certificate common name.
* cafile?: scalar|null, // A certificate authority file.
* capath?: scalar|null, // A directory that contains multiple certificate authority files.
* local_cert?: scalar|null, // A PEM formatted certificate file.
* local_pk?: scalar|null, // A private key file.
* passphrase?: scalar|null, // The passphrase used to encrypt the "local_pk" file.
* ciphers?: scalar|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...).
* peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es).
* sha1?: mixed,
* pin-sha256?: mixed,
* md5?: mixed,
* },
* crypto_method?: scalar|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.
* extra?: array<string, mixed>,
* rate_limiter?: scalar|null, // Rate limiter name to use for throttling requests. // Default: null
* caching?: bool|array{ // Caching configuration.
* enabled?: bool, // Default: false
* cache_pool?: string, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client"
* shared?: bool, // Indicates whether the cache is shared (public) or private. // Default: true
* max_ttl?: int, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null
* },
* retry_failed?: bool|array{
* enabled?: bool, // Default: false
* retry_strategy?: scalar|null, // service id to override the retry strategy. // Default: null
* http_codes?: array<string, array{ // Default: []
* code?: int,
* methods?: list<string>,
* }>,
* max_retries?: int, // Default: 3
* delay?: int, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000
* multiplier?: float, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2
* max_delay?: int, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0
* jitter?: float, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1
* },
* }>,
* },
* mailer?: bool|array{ // Mailer configuration
* enabled?: bool, // Default: false
* message_bus?: scalar|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null
* dsn?: scalar|null, // Default: null
* transports?: array<string, scalar|null>,
* envelope?: array{ // Mailer Envelope configuration
* sender?: scalar|null,
* recipients?: list<scalar|null>,
* allowed_recipients?: list<scalar|null>,
* },
* headers?: array<string, string|array{ // Default: []
* value?: mixed,
* }>,
* dkim_signer?: bool|array{ // DKIM signer configuration
* enabled?: bool, // Default: false
* key?: scalar|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: ""
* domain?: scalar|null, // Default: ""
* select?: scalar|null, // Default: ""
* passphrase?: scalar|null, // The private key passphrase // Default: ""
* options?: array<string, mixed>,
* },
* smime_signer?: bool|array{ // S/MIME signer configuration
* enabled?: bool, // Default: false
* key?: scalar|null, // Path to key (in PEM format) // Default: ""
* certificate?: scalar|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: ""
* passphrase?: scalar|null, // The private key passphrase // Default: null
* extra_certificates?: scalar|null, // Default: null
* sign_options?: int, // Default: null
* },
* smime_encrypter?: bool|array{ // S/MIME encrypter configuration
* enabled?: bool, // Default: false
* repository?: scalar|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: ""
* cipher?: int, // A set of algorithms used to encrypt the message // Default: null
* },
* },
* secrets?: bool|array{
* enabled?: bool, // Default: true
* vault_directory?: scalar|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%"
* local_dotenv_file?: scalar|null, // Default: "%kernel.project_dir%/.env.%kernel.runtime_environment%.local"
* decryption_env_var?: scalar|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET"
* },
* notifier?: bool|array{ // Notifier configuration
* enabled?: bool, // Default: false
* message_bus?: scalar|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null
* chatter_transports?: array<string, scalar|null>,
* texter_transports?: array<string, scalar|null>,
* notification_on_failed_messages?: bool, // Default: false
* channel_policy?: array<string, string|list<scalar|null>>,
* admin_recipients?: list<array{ // Default: []
* email?: scalar|null,
* phone?: scalar|null, // Default: ""
* }>,
* },
* rate_limiter?: bool|array{ // Rate limiter configuration
* enabled?: bool, // Default: false
* limiters?: array<string, array{ // Default: []
* lock_factory?: scalar|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto"
* cache_pool?: scalar|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter"
* storage_service?: scalar|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null
* policy: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit", // The algorithm to be used by this limiter.
* limiters?: list<scalar|null>,
* limit?: int, // The maximum allowed hits in a fixed interval or burst.
* interval?: scalar|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
* rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket".
* interval?: scalar|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).
* amount?: int, // Amount of tokens to add each interval. // Default: 1
* },
* }>,
* },
* uid?: bool|array{ // Uid configuration
* enabled?: bool, // Default: false
* default_uuid_version?: 7|6|4|1, // Default: 7
* name_based_uuid_version?: 5|3, // Default: 5
* name_based_uuid_namespace?: scalar|null,
* time_based_uuid_version?: 7|6|1, // Default: 7
* time_based_uuid_node?: scalar|null,
* },
* html_sanitizer?: bool|array{ // HtmlSanitizer configuration
* enabled?: bool, // Default: false
* sanitizers?: array<string, array{ // Default: []
* allow_safe_elements?: bool, // Allows "safe" elements and attributes. // Default: false
* allow_static_elements?: bool, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false
* allow_elements?: array<string, mixed>,
* block_elements?: list<string>,
* drop_elements?: list<string>,
* allow_attributes?: array<string, mixed>,
* drop_attributes?: array<string, mixed>,
* force_attributes?: array<string, array<string, string>>,
* force_https_urls?: bool, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false
* allowed_link_schemes?: list<string>,
* allowed_link_hosts?: list<string>|null,
* allow_relative_links?: bool, // Allows relative URLs to be used in links href attributes. // Default: false
* allowed_media_schemes?: list<string>,
* allowed_media_hosts?: list<string>|null,
* allow_relative_medias?: bool, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false
* with_attribute_sanitizers?: list<string>,
* without_attribute_sanitizers?: list<string>,
* max_input_length?: int, // The maximum length allowed for the sanitized input. // Default: 0
* }>,
* },
* webhook?: bool|array{ // Webhook configuration
* enabled?: bool, // Default: false
* message_bus?: scalar|null, // The message bus to use. // Default: "messenger.default_bus"
* routing?: array<string, array{ // Default: []
* service: scalar|null,
* secret?: scalar|null, // Default: ""
* }>,
* },
* remote-event?: bool|array{ // RemoteEvent configuration
* enabled?: bool, // Default: false
* },
* json_streamer?: bool|array{ // JSON streamer configuration
* enabled?: bool, // Default: false
* },
* }
* @psalm-type ConfigType = array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* "when@dev"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* },
* "when@prod"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* },
* "when@test"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* ...<string, ExtensionType>,
* }>
* }
*/
final class App
{
/**
* @param ConfigType $config
*
* @psalm-return ConfigType
*/
public static function config(array $config): array
{
return AppReference::config($config);
}
}
namespace Symfony\Component\Routing\Loader\Configurator;
/**
* This class provides array-shapes for configuring the routes of an application.
*
* Example:
*
* ```php
* // config/routes.php
* namespace Symfony\Component\Routing\Loader\Configurator;
*
* return Routes::config([
* 'controllers' => [
* 'resource' => 'routing.controllers',
* ],
* ]);
* ```
*
* @psalm-type RouteConfig = array{
* path: string|array<string,string>,
* controller?: string,
* methods?: string|list<string>,
* requirements?: array<string,string>,
* defaults?: array<string,mixed>,
* options?: array<string,mixed>,
* host?: string|array<string,string>,
* schemes?: string|list<string>,
* condition?: string,
* locale?: string,
* format?: string,
* utf8?: bool,
* stateless?: bool,
* }
* @psalm-type ImportConfig = array{
* resource: string,
* type?: string,
* exclude?: string|list<string>,
* prefix?: string|array<string,string>,
* name_prefix?: string,
* trailing_slash_on_root?: bool,
* controller?: string,
* methods?: string|list<string>,
* requirements?: array<string,string>,
* defaults?: array<string,mixed>,
* options?: array<string,mixed>,
* host?: string|array<string,string>,
* schemes?: string|list<string>,
* condition?: string,
* locale?: string,
* format?: string,
* utf8?: bool,
* stateless?: bool,
* }
* @psalm-type AliasConfig = array{
* alias: string,
* deprecated?: array{package:string, version:string, message?:string},
* }
* @psalm-type RoutesConfig = array{
* "when@dev"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* "when@prod"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* "when@test"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* ...<string, RouteConfig|ImportConfig|AliasConfig>
* }
*/
final class Routes
{
/**
* @param RoutesConfig $config
*
* @psalm-return RoutesConfig
*/
public static function config(array $config): array
{
return $config;
}
}

View File

@@ -1,11 +0,0 @@
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
# This file is the entry point to configure the routes of your app.
# Methods with the #[Route] attribute are automatically imported.
# See also https://symfony.com/doc/current/routing.html
# To list all registered routes, run the following command:
# bin/console debug:router
controllers:
resource: routing.controllers

View File

@@ -1,4 +0,0 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error

View File

@@ -1,23 +0,0 @@
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View File

@@ -1,9 +0,0 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

View File

@@ -1,11 +0,0 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

View File

@@ -1,60 +0,0 @@
{
"symfony/console": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "5.3",
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
},
"files": [
"bin/console"
]
},
"symfony/flex": {
"version": "2.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.4",
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
},
"files": [
".env",
".env.dev"
]
},
"symfony/framework-bundle": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.4",
"ref": "09f6e081c763a206802674ce0cb34a022f0ffc6d"
},
"files": [
"config/packages/cache.yaml",
"config/packages/framework.yaml",
"config/preload.php",
"config/routes/framework.yaml",
"config/services.yaml",
"public/index.php",
"src/Controller/.gitignore",
"src/Kernel.php",
".editorconfig"
]
},
"symfony/routing": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.4",
"ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded"
},
"files": [
"config/packages/routing.yaml",
"config/routes.yaml"
]
}
}