Bladeren bron

Switch default database to SQLite and add phpLiteAdmin

Sets sqlite as the default DB_CONNECTION, teaches SqliteMigration how
to alter/drop columns via SQLite's table-rebuild pattern (previously
unsupported), and fixes container startup so the sqlite file is owned
by www-data instead of root (was causing "readonly database" errors
on writes). Also adds phpLiteAdmin on port 8081 for browsing the
sqlite database, with a fix for nginx dropping the port from
absolute-URL redirects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
main
Daniel Covington 1 week geleden
bovenliggende
commit
581b330c26
9 gewijzigde bestanden met toevoegingen van 204 en 39 verwijderingen
  1. +1
    -1
      .env.example
  2. +6
    -0
      Dockerfile
  3. +1
    -1
      config/database.php
  4. +3
    -1
      docker-compose.yml
  5. +27
    -0
      docker/nginx/phpliteadmin.conf
  6. +5
    -0
      docker/phpliteadmin/phpliteadmin.config.php
  7. +16
    -0
      docker/start-container.sh
  8. +121
    -36
      framework/Database/Migration/SqliteMigration.php
  9. +24
    -0
      readme.md

+ 1
- 1
.env.example Bestand weergeven

@@ -1,7 +1,7 @@
APP_ENV=
APP_HOST=
APP_PORT=
DB_CONNECTION=mysql
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=pro-php-mvc


+ 6
- 0
Dockerfile Bestand weergeven

@@ -8,14 +8,20 @@ RUN apt-get update \

COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

RUN git clone --depth 1 https://github.com/phpLiteAdmin/pla.git /var/www/phpliteadmin \
&& rm -rf /var/www/phpliteadmin/.git /var/www/phpliteadmin/docs

WORKDIR /var/www/html

COPY . /var/www/html
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY docker/nginx/phpliteadmin.conf /etc/nginx/conf.d/phpliteadmin.conf
COPY docker/php/app.ini /usr/local/etc/php/conf.d/app.ini
COPY docker/phpliteadmin/phpliteadmin.config.php /var/www/phpliteadmin/phpliteadmin.config.php
COPY docker/start-container.sh /usr/local/bin/start-container

RUN chmod +x /usr/local/bin/start-container \
&& chown -R www-data:www-data /var/www/phpliteadmin \
&& mkdir -p /run/php

EXPOSE 80


+ 1
- 1
config/database.php Bestand weergeven

@@ -1,7 +1,7 @@
<?php

return [
'default' => env('DB_CONNECTION', 'mysql'),
'default' => env('DB_CONNECTION', 'sqlite'),
'mysql' => [
'type' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),


+ 3
- 1
docker-compose.yml Bestand weergeven

@@ -9,6 +9,7 @@ services:
- db
ports:
- "8080:80"
- "8081:8081"
volumes:
- ./:/var/www/html
- vendor_data:/var/www/html/vendor
@@ -16,13 +17,14 @@ services:
APP_ENV: dev
APP_HOST: 0.0.0.0
APP_PORT: 80
DB_CONNECTION: mysql
DB_CONNECTION: sqlite
DB_HOST: db
DB_PORT: 3306
DB_DATABASE: pro-php-mvc
DB_USERNAME: root
DB_PASSWORD: root
COMPOSER_ALLOW_SUPERUSER: 1
PHPLITEADMIN_PASSWORD: admin

db:
image: mysql:8.0


+ 27
- 0
docker/nginx/phpliteadmin.conf Bestand weergeven

@@ -0,0 +1,27 @@
server {
listen 8081;
server_name _;

root /var/www/phpliteadmin;
index index.php;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
include fastcgi_params;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
# fastcgi_params strips the port from HTTP_HOST for security; reattach
# nginx's own listening port (not the client-supplied one) so
# phpLiteAdmin's absolute redirects/links keep the :8081.
fastcgi_param HTTP_HOST $host:$server_port;
fastcgi_pass 127.0.0.1:9000;
}

location ~ /\. {
deny all;
}
}

+ 5
- 0
docker/phpliteadmin/phpliteadmin.config.php Bestand weergeven

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

$password = getenv('PHPLITEADMIN_PASSWORD') ?: 'admin';
$directory = '/var/www/html/database';
$subdirectories = false;

+ 16
- 0
docker/start-container.sh Bestand weergeven

@@ -11,6 +11,22 @@ if [ ! -f vendor/autoload.php ]; then
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
fi

if [ "${DB_CONNECTION:-mysql}" = "sqlite" ]; then
mkdir -p database
touch database/database.sqlite
chown www-data:www-data database/database.sqlite
chmod 664 database/database.sqlite

if ! php -r '
$pdo = new PDO("sqlite:database/database.sqlite");
$statement = $pdo->query("SELECT name FROM sqlite_master WHERE type='\''table'\'' AND name='\''migrations'\''");

exit($statement->fetch() ? 0 : 1);
'; then
php command.php migrate
fi
fi

if [ "${DB_CONNECTION:-mysql}" = "mysql" ]; then
php -r '
$host = getenv("DB_HOST") ?: "db";


+ 121
- 36
framework/Database/Migration/SqliteMigration.php Bestand weergeven

@@ -18,6 +18,7 @@ class SqliteMigration extends Migration
protected SqliteConnection $connection;
protected string $table;
protected string $type;
protected array $drops = [];

public function __construct(SqliteConnection $connection, string $table, string $type)
{
@@ -28,52 +29,135 @@ class SqliteMigration extends Migration

public function execute()
{
$command = $this->type === 'create' ? '' : 'ALTER TABLE';
if ($this->type === 'create') {
$this->executeCreate();
return;
}

$fields = array_map(fn($field) => $this->stringForField($field), $this->fields);
$hasAlteredField = array_reduce($this->fields, fn($carry, $field) => $carry || $field->alter, false);

if ($this->type === 'create') {
$fields = join(',' . PHP_EOL, $fields);
if (count($this->drops) > 0 || $hasAlteredField) {
$this->executeRebuild();
return;
}

$this->executeAdd();
}

private function executeCreate()
{
$fields = join(',' . PHP_EOL, array_map(fn($field) => $this->columnDefinition($field), $this->fields));

$query = "
CREATE TABLE \"{$this->table}\" (
{$fields}
);
";

$this->connection->pdo()->prepare($query)->execute();
}

$query = "
CREATE TABLE \"{$this->table}\" (
{$fields}
);
";
private function executeAdd()
{
$fields = join(';' . PHP_EOL, array_map(
fn($field) => "ADD COLUMN {$this->columnDefinition($field)}",
$this->fields
));

$query = "
ALTER TABLE \"{$this->table}\"
{$fields};
";

$this->connection->pdo()->prepare($query)->execute();
}

/**
* SQLite can't alter or drop columns in place, so the standard approach is to
* rebuild the table: create a new table with the desired schema, copy the
* surviving data across, then swap it in for the original.
*/
private function executeRebuild()
{
$pdo = $this->connection->pdo();

$existingColumns = $pdo->query("PRAGMA table_info(\"{$this->table}\")")->fetchAll();

$alteredFields = [];
foreach ($this->fields as $field) {
if ($field->alter) {
$alteredFields[$field->name] = $field;
}
}

if ($this->type === 'alter') {
$fields = join(';' . PHP_EOL, $fields);
$newFields = array_filter($this->fields, fn($field) => !$field->alter);

$keptColumnNames = [];
$definitions = [];

$query = "
ALTER TABLE \"{$this->table}\"
{$fields};
";
foreach ($existingColumns as $column) {
$name = $column['name'];

if (in_array($name, $this->drops)) {
continue;
}

$keptColumnNames[] = $name;

$definitions[] = isset($alteredFields[$name])
? $this->columnDefinition($alteredFields[$name])
: $this->existingColumnDefinition($column);
}

foreach ($newFields as $field) {
$definitions[] = $this->columnDefinition($field);
}

$statement = $this->connection->pdo()->prepare($query);
$statement->execute();
$tempTable = "{$this->table}_migration_tmp";
$definitionList = join(',' . PHP_EOL, $definitions);
$columnList = join(', ', array_map(fn($name) => "\"{$name}\"", $keptColumnNames));

$pdo->beginTransaction();

$pdo->exec("CREATE TABLE \"{$tempTable}\" ({$definitionList})");
$pdo->exec("INSERT INTO \"{$tempTable}\" ({$columnList}) SELECT {$columnList} FROM \"{$this->table}\"");
$pdo->exec("DROP TABLE \"{$this->table}\"");
$pdo->exec("ALTER TABLE \"{$tempTable}\" RENAME TO \"{$this->table}\"");

$pdo->commit();
}

private function stringForField(Field $field): string
private function existingColumnDefinition(array $column): string
{
$prefix = '';
$name = $column['name'];
$type = $column['type'];

if ($this->type === 'alter') {
$prefix = 'ADD COLUMN';
if ($column['pk'] == 1 && $type === 'INTEGER') {
return "\"{$name}\" INTEGER PRIMARY KEY AUTOINCREMENT";
}

if ($field->alter) {
throw new MigrationException('SQLite doesn\'t support altering columns');
$definition = "\"{$name}\" {$type}";

if ($column['notnull'] == 1) {
$definition .= " NOT NULL";
}

if ($column['dflt_value'] !== null) {
$definition .= " DEFAULT {$column['dflt_value']}";
}

return $definition;
}

private function columnDefinition(Field $field): string
{
if ($field instanceof BoolField) {
$template = "{$prefix} \"{$field->name}\" INTEGER";
$template = "\"{$field->name}\" INTEGER";

if (!$field->nullable) {
$template .= " NOT NULL";
}
if ($field->default !== null) {
$default = (int) $field->default;
$template .= " DEFAULT {$default}";
@@ -83,12 +167,12 @@ class SqliteMigration extends Migration
}

if ($field instanceof DateTimeField) {
$template = "{$prefix} \"{$field->name}\" TEXT";
$template = "\"{$field->name}\" TEXT";

if (!$field->nullable) {
$template .= " NOT NULL";
}
if ($field->default === 'CURRENT_TIMESTAMP') {
$template .= " DEFAULT CURRENT_TIMESTAMP";
} else if ($field->default !== null) {
@@ -99,12 +183,12 @@ class SqliteMigration extends Migration
}

if ($field instanceof FloatField) {
$template = "{$prefix} \"{$field->name}\" REAL";
$template = "\"{$field->name}\" REAL";

if (!$field->nullable) {
$template .= " NOT NULL";
}
if ($field->default !== null) {
$template .= " DEFAULT {$field->default}";
}
@@ -113,16 +197,16 @@ class SqliteMigration extends Migration
}

if ($field instanceof IdField) {
return "{$prefix} \"{$field->name}\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE";
return "\"{$field->name}\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE";
}

if ($field instanceof IntField) {
$template = "{$prefix} \"{$field->name}\" INTEGER";
$template = "\"{$field->name}\" INTEGER";

if (!$field->nullable) {
$template .= " NOT NULL";
}
if ($field->default !== null) {
$template .= " DEFAULT {$field->default}";
}
@@ -131,14 +215,14 @@ class SqliteMigration extends Migration
}

if ($field instanceof StringField || $field instanceof TextField) {
$template = "{$prefix} \"{$field->name}\" TEXT";
$template = "\"{$field->name}\" TEXT";

if (!$field->nullable) {
$template .= " NOT NULL";
}
if ($field->default !== null) {
$template .= " DEFAULT '{$field->default}'";
$template .= " DEFAULT '{$field->default}'";
}

return $template;
@@ -149,6 +233,7 @@ class SqliteMigration extends Migration

public function dropColumn(string $name): static
{
throw new MigrationException('SQLite doesn\'t support dropping columns');
$this->drops[] = $name;
return $this;
}
}

+ 24
- 0
readme.md Bestand weergeven

@@ -11,3 +11,27 @@ To make requests with cURL, try:
```
curl -X DELETE http://127.0.0.1:8000/old-home
```

## Running with Docker

```
docker compose up -d --build
```

This starts two services:

- **app** — nginx + php-fpm, served at http://localhost:8080
- **db** — MySQL 8, exposed on host port `33060` (only used if `DB_CONNECTION=mysql`)

The default database connection is **SQLite** (`database/database.sqlite`), configured via `DB_CONNECTION` in `.env` / `docker-compose.yml`. On first boot the `app` container automatically creates the SQLite file with the correct permissions and runs migrations. To switch to MySQL instead, set `DB_CONNECTION=mysql` in `docker-compose.yml`.

To run migrations manually:

```
docker compose exec app php command.php migrate
docker compose exec app php command.php migrate --fresh # drops all tables first
```

### phpLiteAdmin

A SQLite admin UI is available at http://localhost:8081, password `admin` (change it via the `PHPLITEADMIN_PASSWORD` environment variable in `docker-compose.yml`). It's a development-only tool — don't expose port 8081 outside your local machine.

Laden…
Annuleren
Opslaan

Powered by TurnKey Linux.