Search by

dmvdigital / dmvapp

demve

Micro framework PHP para shared hosting (FTP, sin CLI).

Package info

github.com/dmvdigital/dmvapp

Type:project

pkg:composer/dmvdigital/dmvapp

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-10-29 04:12 UTC

This package is auto-updated.

Last update: 2026-11-02 17:09:09 UTC


README

Micro framework PHP para prototipar rápido en shared hosting (cPanel/LAMP) con deploy por FTP, sin CLI ni SSH. Slim 4 (rutas) · Eloquent (ORM + migraciones) · phpsfc (vistas) · PHP-DI · respect/validation.

Estructura

public/                     → docroot del hosting (public_html). Solo esto es accesible por URL.
  index.php                 carga public_config.php y arranca la app
  public_config.php         APP_PUBLIC_URL, APP_PUBLIC_DIR, APP_DIR  (uno por entorno, no se sobreescribe)
  .htaccess                 rewrite a index.php, bloquea dotfiles
  uploads/                  archivos subidos (.htaccess: sin ejecución de scripts)
  assets/                   generado por phpsfc (asset(), bundles css/js)

app/                        → APP_DIR, FUERA del docroot (ej: /home/usuario/app)
  .env                      claves y flags (ver .env.example)
  core/                     framework. Se reemplaza entero al actualizar. Incluye core/vendor/
  src/                      tu app (namespace App\, PSR-4)
    routes.php              rutas web (HTML)
    routes_api.php          rutas API, montadas en /api (JSON {error, output})
    config.php              timezone, time_limit, memory_limit, headers de seguridad
    cron.php                tareas periódicas (expresiones cron)
    error_view.php          OPCIONAL: página de error propia (PHP plano)
    container.php           definiciones PHP-DI propias
    Controllers/ Models/    clases App\...
    views/                  componentes phpsfc (pueden sobreescribir core/views)
    migrations/ seeds/
    lang/                   overrides de mensajes de validación
    composer.json, vendor/  OPCIONAL: dependencias propias del proyecto
  .cache/                   vistas, sesiones, rate limit, estado del cron (generado)
  .MAINTENANCE              OPCIONAL: si existe, el sitio responde 503 (ver Mantención)
  logs/                     app-AAAA-MM-DD.log (generado)
  database.sqlite           si usas SQLite (ruta configurable con DB_DATABASE)

Local

Requiere PHP 8.3+ (probado en 8.3 y 8.5) y Composer.

composer create-project dmvdigital/dmvapp miproyecto

O desde un clon: composer install en la raíz. Ambos instalan app/core/vendor y crean app/.env desde .env.example con MIGRATE_TOKEN y ADMIN_PASSWORD random (si .env ya existe no se toca). El composer.json de la raíz solo lanza ese setup: su vendor/ queda vacío y no se sube.

php -S localhost:8000 -t public

Luego http://localhost:8000/_migrate → token de MIGRATE_TOKEN → up y seed (crea el admin de ADMIN_EMAIL/ADMIN_PASSWORD).

Chequeo rápido del core (sin servidor):

php app/core/tests/selfcheck.php

Para que create-project funcione por nombre, el repo debe estar público y registrado en Packagist con un tag (v1.0.0). Sin Packagist: agrega --repository='{"type":"vcs","url":"https://github.com/usuario/dmvapp"}'.

Deploy por FTP

Antes de subir, en local: cd app/core && composer install --no-dev -o (y lo mismo en app/src si tiene composer.json).

Primer deploy:

  1. Sube app/ a una carpeta fuera del docroot (ej. /home/usuario/app), sin .cache/, logs/ ni database.sqlite local.
  2. Sube el contenido de public/ a public_html/ (o la subcarpeta del proyecto).
  3. En el servidor, edita public_html/public_config.php: APP_PUBLIC_URL (con https y subcarpeta si aplica, sin slash final), APP_DIR (ruta absoluta a la carpeta app).
  4. Crea app/.env desde .env.example: APP_ENV=production, APP_DEBUG=false, datos MySQL de cPanel, MIGRATE_TOKEN largo y random (MIGRATE_ACTIONS=status,up recomendado).
  5. Abre https://tusitio.cl/_migrate, ejecuta up (y seed si lo dejaste habilitado).
  6. Opcional: vacía MIGRATE_TOKEN cuando no lo uses; la ruta deja de existir.

Permisos de escritura (PHP): app/.cache/, app/logs/, public_html/uploads/, public_html/assets/ y la carpeta de la sqlite si la usas.

Actualizar:

  • Tu código: sube los archivos cambiados de app/src/ y public/. Si cambiaste vistas y algo raro pasa, borra app/.cache/views/.
  • Framework: borra app/core/ en el servidor y sube el core/ nuevo completo (trae su vendor/). src/ no se toca.
  • Dependencias del proyecto: sube app/src/vendor/.

Clave olvidada o cuenta bloqueada (no hay mail de recuperación): pon un RESET_TOKEN largo y random en app/.env por FTP, abre https://tusitio.cl/_reset-password, ingresa token + email. Genera una clave nueva, desbloquea la cuenta y la muestra una sola vez en pantalla (no queda en .env ni en logs). Después vacía RESET_TOKEN.

Rutas internas (/_migrate, /_reset-password, /_logs, /_cache, /_cron): token por POST, nunca en la URL. 5 tokens inválidos desde una IP bloquean esa IP 15 min en todas (borra app/.cache/ratelimit/ por FTP si te bloqueaste).

  • /_logs: últimas 500 líneas de cada logs/app-*.log, filtro por nivel. Solo lectura.
  • /_cache: vacía app/.cache por tipo (views, sessions = cierra la sesión de todos, ratelimit, cron) o completo.
  • /_reset-password: además de la clave nueva, cierra las sesiones de esa cuenta (y opcionalmente las de todos).

Mantención

Sube un archivo app/.MAINTENANCE: todo responde 503. Vacío = mensaje por defecto; con contenido = se sirve tal cual (tu HTML). En /api responde {"error": "Sitio en mantención...", "output": null}. Las rutas /_* siguen funcionando (para migrar durante el deploy). Bórralo para volver.

Cron y cola

En cPanel → Cron Jobs, cada minuto (o cada 5/15; las tareas se ponen al día):

php /home/usuario/public_html/index.php cron

Sin cron con PHP: un webcron que haga POST /_cron con token=CRON_TOKEN. Cada corrida ejecuta las tareas vencidas de src/cron.php y después procesa la cola (dispatch()), hasta CRON_SECONDS. Corridas superpuestas se saltan.

Si el hosting obliga a dejar app/ dentro del docroot, app/.htaccess bloquea el acceso, pero fuera es lo correcto.

Varias apps, un core (pseudo multi tenant)

public_config.php acepta dos overrides opcionales:

define('APP_CORE_DIR', '/home/usuario/dmvapp-core'); // default: APP_DIR/core
define('APP_SRC_DIR', '/home/usuario/tienda-src');   // default: APP_DIR/src

Lo propio de cada app sigue en su APP_DIR: .env, .cache/ (vistas y sesiones), logs/, sqlite. Cada docroot tiene su uploads/ y assets/.

/home/usuario/
  dmvapp-core/          core compartido (con su vendor/)
  tienda-src/           src compartido por tienda-a y tienda-b
  tienda-a/  tienda-b/  APP_DIR de cada una: .env, .cache, logs, sqlite
  blog/                 APP_DIR con su propio src/ (sin APP_SRC_DIR)
  public_html/tienda-a/ public_html/tienda-b/ public_html/blog/
  • Actualizar el core compartido actualiza todas las apps a la vez: prueba cada src contra el core nuevo antes de subirlo. Para migrar de a una, sube el core nuevo a otra carpeta (dmvapp-core-2) y cambia APP_CORE_DIR app por app.
  • Con src compartido, la diferencia entre apps va en .env (DB, mail, flags). Las migraciones se corren en el /_migrate de cada app.
  • Todas las apps corren con el mismo usuario del hosting: es separación de datos y config, no de seguridad.

Configuración (.env)

Variable Default
APP_ENV / APP_DEBUG – / false debug muestra errores en pantalla: traza en web, detail en JSON (solo 5xx)
APP_LOCALE es mensajes de respect/validation (core/lang/ + overrides de src/lang/validation.{locale}.php); sin archivo = inglés
SESSION_ENABLED true sesión nativa, guardada en .cache/sessions
CSRF_ENABLED true requiere sesión; csrf_field() en cada form POST
AUTH_ENABLED true requiere sesión; agrega migración core_users
LOG_ENABLED true logs/app-AAAA-MM-DD.log
DB_CONNECTION – sqlite o mysql; vacío = sin DB
DB_DATABASE database.sqlite sqlite: ruta relativa a APP_DIR; mysql: nombre de la DB
MIGRATE_TABLE core_migrations tabla de control
MIGRATE_TOKEN – vacío = /_migrate no existe
MIGRATE_ACTIONS status,up * o lista de status,up,rollback,seed
RESET_TOKEN – vacío = /_reset-password no existe
RESET_EMAILS ADMIN_EMAIL cuentas que se pueden reiniciar: lista o *
LOGS_TOKEN / CACHE_TOKEN / CRON_TOKEN – vacío = /_logs, /_cache, /_cron no existen
CRON_SECONDS 50 tiempo máximo por corrida del cron
HEADERS_ENABLED true headers de seguridad (src/config.php)
API_CSRF true false = /api sin CSRF (APIs para terceros)
QUEUE_ENABLED / QUEUE_TRIES true / 3 cola en tabla core_jobs (requiere DB; agrega su migración)
MAIL_DRIVER native native (mail()), http (API, formato Resend), off
MAIL_FROM, MAIL_FROM_NAME, MAIL_API_URL, MAIL_API_KEY

Valores con # van entre comillas.

Uso

// src/routes.php
$app->get('/posts/{id}', function ($req, $res, $args) {
    return view($res, 'pages/post', ['post' => App\Models\Post::findOrFail($args['id'])]);
});
$app->get('/admin', [App\Controllers\AdminController::class, 'index'])->add(Core\Http\AuthMiddleware::class);

Helpers (core/helpers.php): env(), config(), app(), view(), redirect(), url(), app_path(), public_path(), e(), csrf_field(), csrf_exempt(), flash(), old(), errors(), validate(), back_with_errors(), auth(), rate_limit(), api_fail(), dispatch().

Una ruta web puede devolver un Response o un string (HTML). /api y /_ están reservados: van en routes_api.php y en el core.

API

// src/routes_api.php → /api/posts/{id}
return function (Slim\Routing\RouteCollectorProxy $api) {
    $api->get('/posts/{id}', function ($req, $res, array $args) {
        $post = App\Models\Post::find($args['id']) ?? api_fail('No existe el post', 404);
        return $post; // cualquier valor
    });
};

Siempre responde {"error": "", "output": <valor>}; con error {"error": "mensaje", "output": null} (con api_fail($errores, 422) el error puede ser un array campo => mensaje). 404, 500, etc. en /api también salen así. Devolver un Response lo deja pasar tal cual (descargas, etc). CSRF aplica a /api (manda X-CSRF-Token desde el JS del sitio); API_CSRF=false lo quita para todo /api, csrf_exempt($api->post(...)) para una ruta.

Rate limit por ruta: ->add(new Core\Http\RateLimitMiddleware(5, 60)) (5 por minuto por IP, 429 con Retry-After). Manual: if (!rate_limit("contacto:$email", 3, 3600)) ....

Cola

// src/Jobs/EnviarBienvenida.php
final class EnviarBienvenida { public function handle(array $payload): void { Core\Mail::send($payload['email'], ...); } }

dispatch(App\Jobs\EnviarBienvenida::class, ['email' => $email]);        // próximo cron
dispatch(App\Jobs\EnviarBienvenida::class, ['email' => $email], 3600);  // en 1 hora

Si falla se reintenta (1, 2... min de espera) hasta QUEUE_TRIES; después queda en core_jobs con failed_at y error.

Errores y config

Páginas de error: crea src/error_view.php (PHP plano) con $error_number, $error_title, $error_message y $error_detail (traza, solo con APP_DEBUG=true y 5xx). Sin él se usa core/error_view.php.

src/config.php: timezone, time_limit, memory_limit y headers (se suman a los del core: nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy, CSP mínima, HSTS en https; null quita uno). Una ruta que setea un header lo pisa.

use Respect\Validation\Validator as v;

$errors = validate($data, ['email' => [v::email(), 'Email inválido']]);
if ($errors) return back_with_errors('/contacto', $errors, $data);

Sin mensaje propio, el error sale en español desde core/lang/validation.es.php (email debe ser un email válido). Usa ->setName('Email') en la regla para cambiar el nombre mostrado. Para cambiar textos globalmente, agrega la plantilla en inglés a src/lang/validation.es.php (pisa la de core; un src/lang/validation.{locale}.php también sirve para idiomas que core no trae).

Archivos públicos (Core\PublicFiles): upload($uploadedFile, 'uploads/fotos', ['jpg','png'], $maxBytes), put('data/x.json', $json), copy('src/assets/logo.svg', 'img/logo.svg'), url(), delete(). Validan extensión + MIME, nunca escriben .php/.htaccess y no salen de la carpeta pública. Para assets de componentes, phpsfc ya trae $this->asset().

Mail: Core\Mail::send('a@b.cl', 'Asunto', '<p>html</p>', ['reply_to' => ..., 'attachments' => [...]]). Para otra API HTTP, define Core\Mail::$payload = fn(array $m) => [...].

Editor visual de phpsfc: no viene montado. Ver comentario al final de src/routes.php.