create-prisma-php-app 4.0.0-alpha.52 → 4.0.0-alpha.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.php +4 -4
- package/dist/index.js +1 -1
- package/dist/src/Lib/AI/ChatGPTClient.php +1 -1
- package/dist/src/Lib/Auth/Auth.php +1 -1
- package/dist/src/Lib/PHPMailer/Mailer.php +1 -1
- package/dist/src/app/error.php +1 -1
- package/package.json +1 -1
- package/dist/src/Lib/CacheHandler.php +0 -121
- package/dist/src/Lib/ErrorHandler.php +0 -322
- package/dist/src/Lib/IncludeTracker.php +0 -59
- package/dist/src/Lib/PartialRenderer.php +0 -40
- package/dist/src/Lib/Set.php +0 -102
- package/dist/src/Lib/StateManager.php +0 -127
- package/dist/src/Lib/Validator.php +0 -752
package/dist/bootstrap.php
CHANGED
|
@@ -20,16 +20,16 @@ if (session_status() === PHP_SESSION_NONE) {
|
|
|
20
20
|
|
|
21
21
|
use PPHP\Request;
|
|
22
22
|
use PPHP\PrismaPHPSettings;
|
|
23
|
-
use
|
|
23
|
+
use PPHP\StateManager;
|
|
24
24
|
use Lib\Middleware\AuthMiddleware;
|
|
25
25
|
use Lib\Auth\Auth;
|
|
26
26
|
use PPHP\MainLayout;
|
|
27
27
|
use PPHP\PHPX\TemplateCompiler;
|
|
28
|
-
use
|
|
29
|
-
use
|
|
28
|
+
use PPHP\CacheHandler;
|
|
29
|
+
use PPHP\ErrorHandler;
|
|
30
30
|
use Firebase\JWT\JWT;
|
|
31
31
|
use Firebase\JWT\Key;
|
|
32
|
-
use
|
|
32
|
+
use PPHP\PartialRenderer;
|
|
33
33
|
|
|
34
34
|
final class Bootstrap extends RuntimeException
|
|
35
35
|
{
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.php","metadata.php","not-found.php","error.php"],dockerFiles=[".dockerignore","docker-compose.yml","Dockerfile","apache.conf"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!1,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","src/app/layout.php","src/app/index.php"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/app/layout.php","src/app/index.php","src/app/js/index.js","src/app/css/tailwind.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","docker-compose.yml","Dockerfile"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,websocket:!0,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!0},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/lib/websocket","src/lib/mcp"]},ecommerce:{id:"ecommerce",name:"E-commerce Starter",description:"Full e-commerce application with cart, payments, and admin",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-ecommerce-starter",branch:"main"}},blog:{id:"blog",name:"Blog CMS",description:"Blog content management system",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-blog-starter"}}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),c=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let n=`http://localhost/${c}`;n=n.endsWith("/")?n.slice(0,-1):n;const i=n.replace(/(?<!:)(\/\/+)/g,"/"),o=c.replace(/\/\/+/g,"/");return{bsTarget:`${i}/`,bsPathRewrite:{"^/":`/${o.startsWith("/")?o.substring(1):o}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const c=JSON.parse(fs.readFileSync(t,"utf8"));c.scripts={...c.scripts,projectName:"tsx settings/project-name.ts"};let n=[];if(s.tailwindcss&&(c.scripts={...c.scripts,tailwind:"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch","tailwind:build":"postcss src/app/css/tailwind.css -o src/app/css/styles.css"},n.push("tailwind")),s.websocket&&(c.scripts={...c.scripts,websocket:"tsx settings/restart-websocket.ts"},n.push("websocket")),s.mcp&&(c.scripts={...c.scripts,mcp:"tsx settings/restart-mcp.ts"},n.push("mcp")),s.docker&&(c.scripts={...c.scripts,docker:"docker-compose up"},n.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";c.scripts={...c.scripts,"create-swagger-docs":e}}let i={...c.scripts};i.browserSync="tsx settings/bs-config.ts",i["browserSync:build"]="tsx settings/build.ts",i.dev=`npm-run-all projectName -p browserSync ${n.join(" ")}`,i.build=`npm-run-all${s.tailwindcss?" tailwind:build":""} browserSync:build`,c.scripts=i,c.type="module",fs.writeFileSync(t,JSON.stringify(c,null,2))}async function updateComposerJson(e){checkExcludeFiles(path.join(e,"composer.json"))}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let c=fs.readFileSync(t,"utf8");c+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,c,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const c=fs.existsSync(e),n=c&&fs.statSync(e);if(c&&n&&n.isDirectory()){const c=s.toLowerCase();if(!t.websocket&&c.includes("src\\lib\\websocket"))return;if(!t.mcp&&c.includes("src\\lib\\mcp"))return;if(t.backendOnly&&c.includes("src\\app\\js")||t.backendOnly&&c.includes("src\\app\\css")||t.backendOnly&&c.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&c.includes("src\\app\\swagger-docs"))return;const n=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(n))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((c=>{copyRecursiveSync(path.join(e,c),path.join(s,c),t)}))}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("tailwind.css")||s.includes("styles.css")))return;if(!t.websocket&&s.includes("restart-websocket.ts"))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(!t.docker&&dockerFiles.some((e=>s.includes(e))))return;if(t.backendOnly&&nonBackendFiles.some((e=>s.includes(e))))return;if(!t.backendOnly&&s.includes("route.php"))return;if(t.backendOnly&&!t.swaggerDocs&&s.includes("layout.php"))return;if(!t.swaggerDocs&&s.includes("swagger-config.ts"))return;if(t.tailwindcss&&s.includes("index.css"))return;if((!t.swaggerDocs||!t.prisma)&&(s.includes("auto-swagger-docs.ts")||s.includes("prisma-schema-config.json")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach((({src:s,dest:c})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,c),t)}))}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),c="";s.backendOnly||(s.tailwindcss||(c='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),c+='\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"><\/script>');let n="";s.backendOnly||(n=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${c}`:c),e=e.replace("</head>",`${n}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){return!!updateAnswer?.isUpdate&&(updateAnswer?.excludeFilePath?.includes(e.replace(/\\/g,"/"))??!1)}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});const c=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.docker&&c.push({src:"/.dockerignore",dest:"/.dockerignore"},{src:"/docker-compose.yml",dest:"/docker-compose.yml"},{src:"/Dockerfile",dest:"/Dockerfile"},{src:"/apache.conf",dest:"/apache.conf"}),t.forEach((({src:s,dest:t})=>{const c=path.join(__dirname,s),n=path.join(e,t);if(checkExcludeFiles(n))return;const i=fs.readFileSync(c,"utf8");fs.writeFileSync(n,i,{flag:"w"})})),await executeCopy(e,c,s),await updatePackageJson(e,s),await updateComposerJson(e),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const n=generateAuthSecret(),i=generateHexEncodedKey(),o=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${n}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# PHPMailer SMTP configuration (uncomment and set as needed)\n# SMTP_HOST="smtp.gmail.com" # Your SMTP host\n# SMTP_USERNAME="john.doe@gmail.com" # Your SMTP username\n# SMTP_PASSWORD="123456" # Your SMTP password\n# SMTP_PORT="587" # 587 for TLS, 465 for SSL, or your SMTP port\n# SMTP_ENCRYPTION="ssl" # ssl or tls\n# MAIL_FROM="john.doe@gmail.com" # Sender email address\n# MAIL_FROM_NAME="John Doe" # Sender name\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Local storage key for browser storage (auto-generated if not set).\n# Spaces will be replaced with underscores and converted to lowercase.\nLOCALSTORE_KEY="${i}"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins (CSV or JSON array)\nCORS_ALLOWED_ORIGINS=[]\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${o}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,o)}async function getAnswer(e={}){if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const c={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,websocket:t.features.websocket??!1,prisma:t.features.prisma??!1,docker:t.features.docker??!1,swaggerDocs:t.features.swaggerDocs??!1,mcp:t.features.mcp??!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(c.backendOnly=!0),n.includes("--swagger-docs")&&(c.swaggerDocs=!0),n.includes("--tailwindcss")&&(c.tailwindcss=!0),n.includes("--websocket")&&(c.websocket=!0),n.includes("--mcp")&&(c.mcp=!0),n.includes("--prisma")&&(c.prisma=!0),n.includes("--docker")&&(c.docker=!0),c}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},c=process.argv.slice(2);return c.includes("--backend-only")&&(t.backendOnly=!0),c.includes("--swagger-docs")&&(t.swaggerDocs=!0),c.includes("--tailwindcss")&&(t.tailwindcss=!0),c.includes("--websocket")&&(t.websocket=!0),c.includes("--mcp")&&(t.mcp=!0),c.includes("--prisma")&&(t.prisma=!0),c.includes("--docker")&&(t.docker=!0),t}}const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||s.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{process.exit(0)},c=await prompts(s,{onCancel:t}),n=[];c.backendOnly??e.backendOnly??!1?(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||n.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const i=await prompts(n,{onCancel:t});return{projectName:c.projectName?String(c.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:c.backendOnly??e.backendOnly??!1,swaggerDocs:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,websocket:i.websocket??e.websocket??!1,mcp:i.mcp??e.mcp??!1,prisma:i.prisma??e.prisma??!1,docker:i.docker??e.docker??!1}}async function uninstallNpmDependencies(e,s,t=!1){s.forEach((e=>{}));const c=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(c,{stdio:"inherit",cwd:e})}async function uninstallComposerDependencies(e,s){s.forEach((e=>{}));const t=`C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let c="";e.on("data",(e=>c+=e)),e.on("end",(()=>{try{const e=JSON.parse(c);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}}))})).on("error",(e=>t(e)))}))}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),c=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>c[e])return 1;if(t[e]<c[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:null}catch(e){return null}}
|
|
2
|
+
import{execSync,spawnSync}from"child_process";import fs from"fs";import{fileURLToPath}from"url";import path from"path";import chalk from"chalk";import prompts from"prompts";import https from"https";import{randomBytes}from"crypto";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename);let updateAnswer=null;const nonBackendFiles=["favicon.ico","\\src\\app\\index.php","metadata.php","not-found.php","error.php"],dockerFiles=[".dockerignore","docker-compose.yml","Dockerfile","apache.conf"],STARTER_KITS={basic:{id:"basic",name:"Basic PHP Application",description:"Simple PHP backend with minimal dependencies",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!1,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","src/app/layout.php","src/app/index.php"]},fullstack:{id:"fullstack",name:"Full-Stack Application",description:"Complete web application with frontend and backend",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/app/layout.php","src/app/index.php","src/app/js/index.js","src/app/css/tailwind.css"]},api:{id:"api",name:"REST API",description:"Backend API with database and documentation",features:{backendOnly:!0,tailwindcss:!1,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:["bootstrap.php",".htaccess","docker-compose.yml","Dockerfile"]},realtime:{id:"realtime",name:"Real-time Application",description:"Application with WebSocket support and MCP",features:{backendOnly:!1,tailwindcss:!0,websocket:!0,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!0},requiredFiles:["bootstrap.php",".htaccess","postcss.config.js","src/lib/websocket","src/lib/mcp"]},ecommerce:{id:"ecommerce",name:"E-commerce Starter",description:"Full e-commerce application with cart, payments, and admin",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!0,swaggerDocs:!0,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-ecommerce-starter",branch:"main"}},blog:{id:"blog",name:"Blog CMS",description:"Blog content management system",features:{backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!1,mcp:!1},requiredFiles:[],source:{type:"git",url:"https://github.com/your-org/prisma-php-blog-starter"}}};function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),c=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let n=`http://localhost/${c}`;n=n.endsWith("/")?n.slice(0,-1):n;const i=n.replace(/(?<!:)(\/\/+)/g,"/"),r=c.replace(/\/\/+/g,"/");return{bsTarget:`${i}/`,bsPathRewrite:{"^/":`/${r.startsWith("/")?r.substring(1):r}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const c=JSON.parse(fs.readFileSync(t,"utf8"));c.scripts={...c.scripts,projectName:"tsx settings/project-name.ts"};let n=[];if(s.tailwindcss&&(c.scripts={...c.scripts,tailwind:"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch","tailwind:build":"postcss src/app/css/tailwind.css -o src/app/css/styles.css"},n.push("tailwind")),s.websocket&&(c.scripts={...c.scripts,websocket:"tsx settings/restart-websocket.ts"},n.push("websocket")),s.mcp&&(c.scripts={...c.scripts,mcp:"tsx settings/restart-mcp.ts"},n.push("mcp")),s.docker&&(c.scripts={...c.scripts,docker:"docker-compose up"},n.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";c.scripts={...c.scripts,"create-swagger-docs":e}}let i={...c.scripts};i.browserSync="tsx settings/bs-config.ts",i["browserSync:build"]="tsx settings/build.ts",i.dev=`npm-run-all projectName -p browserSync ${n.join(" ")}`,i.build=`npm-run-all${s.tailwindcss?" tailwind:build":""} browserSync:build`,c.scripts=i,c.type="module",fs.writeFileSync(t,JSON.stringify(c,null,2))}async function updateComposerJson(e){checkExcludeFiles(path.join(e,"composer.json"))}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let c=fs.readFileSync(t,"utf8");c+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,c,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateHexEncodedKey(e=16){return randomBytes(e).toString("hex")}function copyRecursiveSync(e,s,t){const c=fs.existsSync(e),n=c&&fs.statSync(e);if(c&&n&&n.isDirectory()){const c=s.toLowerCase();if(!t.websocket&&c.includes("src\\lib\\websocket"))return;if(!t.mcp&&c.includes("src\\lib\\mcp"))return;if(t.backendOnly&&c.includes("src\\app\\js")||t.backendOnly&&c.includes("src\\app\\css")||t.backendOnly&&c.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&c.includes("src\\app\\swagger-docs"))return;const n=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(n))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((c=>{copyRecursiveSync(path.join(e,c),path.join(s,c),t)}))}else{if(checkExcludeFiles(s))return;if(!t.tailwindcss&&(s.includes("tailwind.css")||s.includes("styles.css")))return;if(!t.websocket&&s.includes("restart-websocket.ts"))return;if(!t.mcp&&s.includes("restart-mcp.ts"))return;if(!t.docker&&dockerFiles.some((e=>s.includes(e))))return;if(t.backendOnly&&nonBackendFiles.some((e=>s.includes(e))))return;if(!t.backendOnly&&s.includes("route.php"))return;if(t.backendOnly&&!t.swaggerDocs&&s.includes("layout.php"))return;if(!t.swaggerDocs&&s.includes("swagger-config.ts"))return;if(t.tailwindcss&&s.includes("index.css"))return;if((!t.swaggerDocs||!t.prisma)&&(s.includes("auto-swagger-docs.ts")||s.includes("prisma-schema-config.json")))return;fs.copyFileSync(e,s,0)}}async function executeCopy(e,s,t){s.forEach((({src:s,dest:c})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,c),t)}))}function modifyPostcssConfig(e){const s=path.join(e,"postcss.config.js");if(checkExcludeFiles(s))return;fs.writeFileSync(s,'export default {\n plugins: {\n "@tailwindcss/postcss": {},\n cssnano: {},\n },\n};',{flag:"w"})}function modifyLayoutPHP(e,s){const t=path.join(e,"src","app","layout.php");if(!checkExcludeFiles(t))try{let e=fs.readFileSync(t,"utf8"),c="";s.backendOnly||(s.tailwindcss||(c='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),c+='\n <script src="<?= Request::baseUrl; ?>/js/morphdom-umd.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"><\/script>');let n="";s.backendOnly||(n=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${c}`:c),e=e.replace("</head>",`${n}\n</head>`),fs.writeFileSync(t,e,{flag:"w"})}catch(e){}}async function createOrUpdateEnvFile(e,s){const t=path.join(e,".env");checkExcludeFiles(t)||fs.writeFileSync(t,s,{flag:"w"})}function checkExcludeFiles(e){return!!updateAnswer?.isUpdate&&(updateAnswer?.excludeFilePath?.includes(e.replace(/\\/g,"/"))??!1)}async function createDirectoryStructure(e,s){const t=[{src:"/bootstrap.php",dest:"/bootstrap.php"},{src:"/.htaccess",dest:"/.htaccess"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});const c=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.docker&&c.push({src:"/.dockerignore",dest:"/.dockerignore"},{src:"/docker-compose.yml",dest:"/docker-compose.yml"},{src:"/Dockerfile",dest:"/Dockerfile"},{src:"/apache.conf",dest:"/apache.conf"}),t.forEach((({src:s,dest:t})=>{const c=path.join(__dirname,s),n=path.join(e,t);if(checkExcludeFiles(n))return;const i=fs.readFileSync(c,"utf8");fs.writeFileSync(n,i,{flag:"w"})})),await executeCopy(e,c,s),await updatePackageJson(e,s),await updateComposerJson(e),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const n=generateAuthSecret(),i=generateHexEncodedKey(),r=`# Authentication secret key for JWT or session encryption.\nAUTH_SECRET="${n}"\n# Name of the authentication cookie.\nAUTH_COOKIE_NAME="${generateHexEncodedKey(8)}"\n\n# Show errors in the browser (development only). Set to false in production.\nSHOW_ERRORS="true"\n\n# Application timezone (default: UTC)\nAPP_TIMEZONE="UTC"\n\n# Application environment (development or production)\nAPP_ENV="development"\n\n# Enable or disable application cache (default: false)\nCACHE_ENABLED="false"\n# Cache time-to-live in seconds (default: 600)\nCACHE_TTL="600"\n\n# Local storage key for browser storage (auto-generated if not set).\n# Spaces will be replaced with underscores and converted to lowercase.\nLOCALSTORE_KEY="${i}"\n\n# Secret key for encrypting function calls.\nFUNCTION_CALL_SECRET="${generateHexEncodedKey(32)}"\n\n# Single or multiple origins (CSV or JSON array)\nCORS_ALLOWED_ORIGINS=[]\n\n# If you need cookies/Authorization across origins, keep this true\nCORS_ALLOW_CREDENTIALS="true"\n\n# Optional tuning\nCORS_ALLOWED_METHODS="GET,POST,PUT,PATCH,DELETE,OPTIONS"\nCORS_ALLOWED_HEADERS="Content-Type,Authorization,X-Requested-With"\nCORS_EXPOSE_HEADERS=""\nCORS_MAX_AGE="86400"`;if(s.prisma){const s=`${'# Environment variables declared in this file are automatically made available to Prisma.\n# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema\n\n# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.\n# See the documentation for all the connection string options: https://pris.ly/d/connection-strings\n\nDATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"'}\n\n${r}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,r)}async function getAnswer(e={}){if(e.starterKit){const s=e.starterKit;let t=null;if(STARTER_KITS[s]&&(t=STARTER_KITS[s]),t){const c={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:t.features.backendOnly??!1,tailwindcss:t.features.tailwindcss??!1,websocket:t.features.websocket??!1,prisma:t.features.prisma??!1,docker:t.features.docker??!1,swaggerDocs:t.features.swaggerDocs??!1,mcp:t.features.mcp??!1},n=process.argv.slice(2);return n.includes("--backend-only")&&(c.backendOnly=!0),n.includes("--swagger-docs")&&(c.swaggerDocs=!0),n.includes("--tailwindcss")&&(c.tailwindcss=!0),n.includes("--websocket")&&(c.websocket=!0),n.includes("--mcp")&&(c.mcp=!0),n.includes("--prisma")&&(c.prisma=!0),n.includes("--docker")&&(c.docker=!0),c}if(e.starterKitSource){const t={projectName:e.projectName??"my-app",starterKit:s,starterKitSource:e.starterKitSource,backendOnly:!1,tailwindcss:!0,websocket:!1,prisma:!0,docker:!1,swaggerDocs:!0,mcp:!1},c=process.argv.slice(2);return c.includes("--backend-only")&&(t.backendOnly=!0),c.includes("--swagger-docs")&&(t.swaggerDocs=!0),c.includes("--tailwindcss")&&(t.tailwindcss=!0),c.includes("--websocket")&&(t.websocket=!0),c.includes("--mcp")&&(t.mcp=!0),c.includes("--prisma")&&(t.prisma=!0),c.includes("--docker")&&(t.docker=!0),t}}const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||updateAnswer?.isUpdate||s.push({type:"toggle",name:"backendOnly",message:`Would you like to create a ${chalk.blue("backend-only project")}?`,initial:!1,active:"Yes",inactive:"No"});const t=()=>{process.exit(0)},c=await prompts(s,{onCancel:t}),n=[];c.backendOnly??e.backendOnly??!1?(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||n.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||n.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||n.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.mcp||n.push({type:"toggle",name:"mcp",message:`Would you like to use ${chalk.blue("MCP (Model Context Protocol)")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||n.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||n.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const i=await prompts(n,{onCancel:t});return{projectName:c.projectName?String(c.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:c.backendOnly??e.backendOnly??!1,swaggerDocs:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,websocket:i.websocket??e.websocket??!1,mcp:i.mcp??e.mcp??!1,prisma:i.prisma??e.prisma??!1,docker:i.docker??e.docker??!1}}async function uninstallNpmDependencies(e,s,t=!1){s.forEach((e=>{}));const c=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(c,{stdio:"inherit",cwd:e})}async function uninstallComposerDependencies(e,s){s.forEach((e=>{}));const t=`C:\\xampp\\php\\php.exe C:\\ProgramData\\ComposerSetup\\bin\\composer.phar remove ${s.join(" ")}`;execSync(t,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let c="";e.on("data",(e=>c+=e)),e.on("end",(()=>{try{const e=JSON.parse(c);s(e["dist-tags"].latest)}catch(e){t(new Error("Failed to parse JSON response"))}}))})).on("error",(e=>t(e)))}))}const readJsonFile=e=>{const s=fs.readFileSync(e,"utf8");return JSON.parse(s)};function compareVersions(e,s){const t=e.split(".").map(Number),c=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>c[e])return 1;if(t[e]<c[e])return-1}return 0}function getInstalledPackageVersion(e){try{const s=execSync(`npm list -g ${e} --depth=0`).toString().match(new RegExp(`${e}@(\\d+\\.\\d+\\.\\d+)`));return s?s[1]:null}catch(e){return null}}
|
|
3
3
|
/**
|
|
4
4
|
* Install dependencies in the specified directory.
|
|
5
5
|
* @param {string} baseDir - The base directory where to install the dependencies.
|
package/dist/src/app/error.php
CHANGED
package/package.json
CHANGED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Lib;
|
|
6
|
-
|
|
7
|
-
use PPHP\PrismaPHPSettings;
|
|
8
|
-
|
|
9
|
-
class CacheHandler
|
|
10
|
-
{
|
|
11
|
-
public static bool $isCacheable = true; // Enable or disable caching by route
|
|
12
|
-
public static int $ttl = 0; // Time to live in seconds (0 = no action taken)
|
|
13
|
-
|
|
14
|
-
private static string $cacheDir = DOCUMENT_PATH . '/caches';
|
|
15
|
-
private static bool $cacheDirChecked = false;
|
|
16
|
-
|
|
17
|
-
private static function ensureCacheDirectoryExists(): void
|
|
18
|
-
{
|
|
19
|
-
if (self::$cacheDirChecked) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!is_dir(self::$cacheDir) && !mkdir(self::$cacheDir, 0777, true) && !is_dir(self::$cacheDir)) {
|
|
24
|
-
die("Error: Unable to create cache directory at: " . self::$cacheDir);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
self::$cacheDirChecked = true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public static function getCacheFilePath(string $uri): string
|
|
31
|
-
{
|
|
32
|
-
$requestFilesData = PrismaPHPSettings::$includeFiles;
|
|
33
|
-
$fileName = $requestFilesData[$uri]['fileName'] ?? '';
|
|
34
|
-
$isCacheable = $requestFilesData[$uri]['isCacheable'] ?? self::$isCacheable;
|
|
35
|
-
|
|
36
|
-
if (!$isCacheable || $fileName === '') {
|
|
37
|
-
return '';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return self::$cacheDir . '/' . $fileName . '.html';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private static function isExpired(string $cacheFile, int $ttlSeconds = 600): bool
|
|
44
|
-
{
|
|
45
|
-
if (!file_exists($cacheFile)) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
$fileAge = time() - filemtime($cacheFile);
|
|
49
|
-
return $fileAge > $ttlSeconds;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Serve cache if available, not expired, and route is marked cacheable.
|
|
54
|
-
* We look up a route-specific TTL if defined, otherwise use a fallback.
|
|
55
|
-
*/
|
|
56
|
-
public static function serveCache(string $uri, int $defaultTtl = 600): void
|
|
57
|
-
{
|
|
58
|
-
if ($uri === '') {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
$requestFilesData = PrismaPHPSettings::$includeFiles;
|
|
63
|
-
|
|
64
|
-
// Get the route-specific TTL if set, or default to 0
|
|
65
|
-
$routeTtl = $requestFilesData[$uri]['cacheTtl'] ?? 0;
|
|
66
|
-
|
|
67
|
-
// If the route has a TTL greater than 0, use that.
|
|
68
|
-
// Otherwise (0 or not defined), use the default.
|
|
69
|
-
$ttlSeconds = ($routeTtl > 0) ? $routeTtl : $defaultTtl;
|
|
70
|
-
|
|
71
|
-
$cacheFile = self::getCacheFilePath($uri);
|
|
72
|
-
|
|
73
|
-
if ($cacheFile === '' || !file_exists($cacheFile) || self::isExpired($cacheFile, $ttlSeconds)) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
echo "<!-- Cached copy generated at: " . date('Y-m-d H:i:s', filemtime($cacheFile)) . " -->\n";
|
|
78
|
-
readfile($cacheFile);
|
|
79
|
-
exit;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
public static function saveCache(string $uri, string $content, bool $useLock = true): void
|
|
83
|
-
{
|
|
84
|
-
if ($uri === '') {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
self::ensureCacheDirectoryExists();
|
|
88
|
-
|
|
89
|
-
$cacheFile = self::getCacheFilePath($uri);
|
|
90
|
-
if ($cacheFile === '') {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
$flags = $useLock ? LOCK_EX : 0;
|
|
95
|
-
$written = @file_put_contents($cacheFile, $content, $flags);
|
|
96
|
-
|
|
97
|
-
if ($written === false) {
|
|
98
|
-
die("Error: Failed to write cache file: $cacheFile");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
public static function resetCache(?string $uri = null): void
|
|
103
|
-
{
|
|
104
|
-
self::ensureCacheDirectoryExists();
|
|
105
|
-
|
|
106
|
-
if ($uri !== null) {
|
|
107
|
-
$cacheFile = self::getCacheFilePath($uri);
|
|
108
|
-
if ($cacheFile !== '' && file_exists($cacheFile)) {
|
|
109
|
-
unlink($cacheFile);
|
|
110
|
-
}
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
$files = glob(self::$cacheDir . '/*.html') ?: [];
|
|
115
|
-
foreach ($files as $file) {
|
|
116
|
-
if (is_file($file)) {
|
|
117
|
-
unlink($file);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Lib;
|
|
6
|
-
|
|
7
|
-
use Bootstrap;
|
|
8
|
-
use PPHP\MainLayout;
|
|
9
|
-
use Throwable;
|
|
10
|
-
use PPHP\PHPX\Exceptions\ComponentValidationException;
|
|
11
|
-
|
|
12
|
-
class ErrorHandler
|
|
13
|
-
{
|
|
14
|
-
public static string $content = '';
|
|
15
|
-
|
|
16
|
-
public static function registerHandlers(): void
|
|
17
|
-
{
|
|
18
|
-
self::registerExceptionHandler();
|
|
19
|
-
self::registerShutdownFunction();
|
|
20
|
-
self::registerErrorHandler();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private static function registerExceptionHandler(): void
|
|
24
|
-
{
|
|
25
|
-
set_exception_handler(function ($exception) {
|
|
26
|
-
$errorContent = Bootstrap::isAjaxOrXFileRequestOrRouteFile()
|
|
27
|
-
? "Exception: " . $exception->getMessage()
|
|
28
|
-
: "<div class='error'>Exception: " . htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8') . "</div>";
|
|
29
|
-
|
|
30
|
-
self::modifyOutputLayoutForError($errorContent);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private static function registerShutdownFunction(): void
|
|
35
|
-
{
|
|
36
|
-
register_shutdown_function(function () {
|
|
37
|
-
$error = error_get_last();
|
|
38
|
-
if (
|
|
39
|
-
$error !== null &&
|
|
40
|
-
in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_RECOVERABLE_ERROR], true)
|
|
41
|
-
) {
|
|
42
|
-
$errorContent = Bootstrap::isAjaxOrXFileRequestOrRouteFile()
|
|
43
|
-
? "Fatal Error: " . $error['message'] . " in " . $error['file'] . " on line " . $error['line']
|
|
44
|
-
: "<div class='error'>Fatal Error: " . htmlspecialchars($error['message'], ENT_QUOTES, 'UTF-8') .
|
|
45
|
-
" in " . htmlspecialchars($error['file'], ENT_QUOTES, 'UTF-8') .
|
|
46
|
-
" on line " . $error['line'] . "</div>";
|
|
47
|
-
|
|
48
|
-
self::modifyOutputLayoutForError($errorContent);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private static function registerErrorHandler(): void
|
|
54
|
-
{
|
|
55
|
-
set_error_handler(function ($severity, $message, $file, $line) {
|
|
56
|
-
if (!(error_reporting() & $severity)) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
$errorContent = Bootstrap::isAjaxOrXFileRequestOrRouteFile()
|
|
60
|
-
? "Error: {$severity} - {$message} in {$file} on line {$line}"
|
|
61
|
-
: "<div class='error'>Error: {$message} in {$file} on line {$line}</div>";
|
|
62
|
-
|
|
63
|
-
if ($severity === E_WARNING || $severity === E_NOTICE) {
|
|
64
|
-
self::modifyOutputLayoutForError($errorContent);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public static function checkFatalError(): void
|
|
70
|
-
{
|
|
71
|
-
$error = error_get_last();
|
|
72
|
-
if (
|
|
73
|
-
$error !== null &&
|
|
74
|
-
in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_RECOVERABLE_ERROR], true)
|
|
75
|
-
) {
|
|
76
|
-
$errorContent = Bootstrap::isAjaxOrXFileRequestOrRouteFile()
|
|
77
|
-
? "Fatal Error: " . $error['message'] . " in " . $error['file'] . " on line " . $error['line']
|
|
78
|
-
: "<div class='error'>Fatal Error: " . htmlspecialchars($error['message'], ENT_QUOTES, 'UTF-8') .
|
|
79
|
-
" in " . htmlspecialchars($error['file'], ENT_QUOTES, 'UTF-8') .
|
|
80
|
-
" on line " . $error['line'] . "</div>";
|
|
81
|
-
|
|
82
|
-
self::modifyOutputLayoutForError($errorContent);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public static function modifyOutputLayoutForError($contentToAdd): void
|
|
87
|
-
{
|
|
88
|
-
$errorFile = APP_PATH . '/error.php';
|
|
89
|
-
$errorFileExists = file_exists($errorFile);
|
|
90
|
-
|
|
91
|
-
if ($_ENV['SHOW_ERRORS'] === "false") {
|
|
92
|
-
if ($errorFileExists) {
|
|
93
|
-
$contentToAdd = Bootstrap::isAjaxOrXFileRequestOrRouteFile() ? "An error occurred" : "<div class='error'>An error occurred</div>";
|
|
94
|
-
} else {
|
|
95
|
-
exit;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if ($errorFileExists) {
|
|
100
|
-
if (ob_get_level()) {
|
|
101
|
-
ob_end_clean();
|
|
102
|
-
}
|
|
103
|
-
self::$content = $contentToAdd;
|
|
104
|
-
if (Bootstrap::isAjaxOrXFileRequestOrRouteFile()) {
|
|
105
|
-
header('Content-Type: application/json');
|
|
106
|
-
echo json_encode(['success' => false, 'error' => self::$content]);
|
|
107
|
-
http_response_code(403);
|
|
108
|
-
} else {
|
|
109
|
-
$layoutFile = APP_PATH . '/layout.php';
|
|
110
|
-
if (file_exists($layoutFile)) {
|
|
111
|
-
ob_start();
|
|
112
|
-
require_once $errorFile;
|
|
113
|
-
MainLayout::$children = ob_get_clean();
|
|
114
|
-
require $layoutFile;
|
|
115
|
-
} else {
|
|
116
|
-
echo self::$content;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
if (Bootstrap::isAjaxOrXFileRequestOrRouteFile()) {
|
|
121
|
-
header('Content-Type: application/json');
|
|
122
|
-
echo json_encode(['success' => false, 'error' => $contentToAdd]);
|
|
123
|
-
http_response_code(403);
|
|
124
|
-
} else {
|
|
125
|
-
echo $contentToAdd;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
exit;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
public static function formatExceptionForDisplay(Throwable $exception): string
|
|
132
|
-
{
|
|
133
|
-
// Handle specific exception types
|
|
134
|
-
if ($exception instanceof ComponentValidationException) {
|
|
135
|
-
return self::formatComponentValidationError($exception);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Handle template compilation errors specifically
|
|
139
|
-
if (strpos($exception->getMessage(), 'Invalid prop') !== false) {
|
|
140
|
-
return self::formatTemplateCompilerError($exception);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Generic exception formatting
|
|
144
|
-
return self::formatGenericException($exception);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private static function formatComponentValidationError(ComponentValidationException $exception): string
|
|
148
|
-
{
|
|
149
|
-
$message = htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8');
|
|
150
|
-
$file = htmlspecialchars($exception->getFile(), ENT_QUOTES, 'UTF-8');
|
|
151
|
-
$line = $exception->getLine();
|
|
152
|
-
|
|
153
|
-
// Get the details from the ComponentValidationException
|
|
154
|
-
$propName = method_exists($exception, 'getPropName') ? $exception->getPropName() : 'unknown';
|
|
155
|
-
$componentName = method_exists($exception, 'getComponentName') ? $exception->getComponentName() : 'unknown';
|
|
156
|
-
$availableProps = method_exists($exception, 'getAvailableProps') ? $exception->getAvailableProps() : [];
|
|
157
|
-
|
|
158
|
-
$availablePropsString = !empty($availableProps) ? implode(', ', $availableProps) : 'none defined';
|
|
159
|
-
|
|
160
|
-
return <<<HTML
|
|
161
|
-
<div class="error-container max-w-4xl mx-auto mt-8 bg-red-50 border border-red-200 rounded-lg shadow-lg">
|
|
162
|
-
<div class="bg-red-100 px-6 py-4 border-b border-red-200">
|
|
163
|
-
<h2 class="text-xl font-bold text-red-800 flex items-center">
|
|
164
|
-
<svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
165
|
-
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
166
|
-
</svg>
|
|
167
|
-
Component Validation Error
|
|
168
|
-
</h2>
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
<div class="p-6">
|
|
172
|
-
<div class="bg-white border border-red-200 rounded-lg p-4 mb-4">
|
|
173
|
-
<div class="mb-3">
|
|
174
|
-
<span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium">
|
|
175
|
-
Component: {$componentName}
|
|
176
|
-
</span>
|
|
177
|
-
<span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium ml-2">
|
|
178
|
-
Invalid Prop: {$propName}
|
|
179
|
-
</span>
|
|
180
|
-
</div>
|
|
181
|
-
<pre class="text-sm text-red-800 whitespace-pre-wrap font-mono">{$message}</pre>
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
<div class="text-sm text-gray-600 mb-4">
|
|
185
|
-
<strong>File:</strong> <code class="bg-gray-100 px-2 py-1 rounded text-xs">{$file}</code><br />
|
|
186
|
-
<strong>Line:</strong> <span class="bg-gray-100 px-2 py-1 rounded text-xs">{$line}</span>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
|
190
|
-
<h4 class="font-medium text-blue-800 mb-2">💡 Available Props:</h4>
|
|
191
|
-
<p class="text-blue-700 text-sm">
|
|
192
|
-
<code class="bg-blue-100 px-2 py-1 rounded text-xs">{$availablePropsString}</code>
|
|
193
|
-
</p>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
|
|
197
|
-
<h4 class="font-medium text-green-800 mb-2">🔧 Quick Fixes:</h4>
|
|
198
|
-
<ul class="text-green-700 text-sm space-y-1">
|
|
199
|
-
<li>• Remove the '<code>{$propName}</code>' prop from your template</li>
|
|
200
|
-
<li>• Add '<code>public \${$propName};</code>' to your <code>{$componentName}</code> component class</li>
|
|
201
|
-
<li>• Use data attributes: '<code>data-{$propName}</code>' instead</li>
|
|
202
|
-
</ul>
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
<details class="mt-4">
|
|
206
|
-
<summary class="cursor-pointer text-red-600 font-medium hover:text-red-800 select-none">
|
|
207
|
-
Show Stack Trace
|
|
208
|
-
</summary>
|
|
209
|
-
<div class="mt-3 bg-gray-50 border border-gray-200 rounded p-4">
|
|
210
|
-
<pre class="text-xs text-gray-700 overflow-auto whitespace-pre-wrap max-h-96">{$exception->getTraceAsString()}</pre>
|
|
211
|
-
</div>
|
|
212
|
-
</details>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
HTML;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private static function formatTemplateCompilerError(Throwable $exception): string
|
|
219
|
-
{
|
|
220
|
-
$message = htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8');
|
|
221
|
-
$file = htmlspecialchars($exception->getFile(), ENT_QUOTES, 'UTF-8');
|
|
222
|
-
$line = $exception->getLine();
|
|
223
|
-
|
|
224
|
-
// Extract the component validation error details
|
|
225
|
-
if (preg_match("/Invalid prop '([^']+)' passed to component '([^']+)'/", $exception->getMessage(), $matches)) {
|
|
226
|
-
$invalidProp = $matches[1];
|
|
227
|
-
$componentName = $matches[2];
|
|
228
|
-
|
|
229
|
-
return <<<HTML
|
|
230
|
-
<div class="error-container max-w-4xl mx-auto mt-8 bg-red-50 border border-red-200 rounded-lg shadow-lg">
|
|
231
|
-
<div class="bg-red-100 px-6 py-4 border-b border-red-200">
|
|
232
|
-
<h2 class="text-xl font-bold text-red-800 flex items-center">
|
|
233
|
-
<svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
234
|
-
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
235
|
-
</svg>
|
|
236
|
-
Template Compilation Error
|
|
237
|
-
</h2>
|
|
238
|
-
</div>
|
|
239
|
-
|
|
240
|
-
<div class="p-6">
|
|
241
|
-
<div class="bg-white border border-red-200 rounded-lg p-4 mb-4">
|
|
242
|
-
<div class="mb-3">
|
|
243
|
-
<span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium">
|
|
244
|
-
Component: {$componentName}
|
|
245
|
-
</span>
|
|
246
|
-
<span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium ml-2">
|
|
247
|
-
Invalid Prop: {$invalidProp}
|
|
248
|
-
</span>
|
|
249
|
-
</div>
|
|
250
|
-
<p class="text-red-800 font-medium">{$message}</p>
|
|
251
|
-
</div>
|
|
252
|
-
|
|
253
|
-
<div class="text-sm text-gray-600 mb-4">
|
|
254
|
-
<strong>File:</strong> {$file}<br />
|
|
255
|
-
<strong>Line:</strong> {$line}
|
|
256
|
-
</div>
|
|
257
|
-
|
|
258
|
-
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
|
259
|
-
<h4 class="font-medium text-blue-800 mb-2">💡 Quick Fix:</h4>
|
|
260
|
-
<p class="text-blue-700 text-sm">
|
|
261
|
-
Either remove the '<code>{$invalidProp}</code>' prop from your template, or add it as a public property to your <code>{$componentName}</code> component class.
|
|
262
|
-
</p>
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
<details class="mt-4">
|
|
266
|
-
<summary class="cursor-pointer text-red-600 font-medium hover:text-red-800 select-none">
|
|
267
|
-
Show Stack Trace
|
|
268
|
-
</summary>
|
|
269
|
-
<div class="mt-3 bg-gray-50 border border-gray-200 rounded p-4">
|
|
270
|
-
<pre class="text-xs text-gray-700 overflow-auto whitespace-pre-wrap">{$exception->getTraceAsString()}</pre>
|
|
271
|
-
</div>
|
|
272
|
-
</details>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
HTML;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Fallback to generic formatting
|
|
279
|
-
return self::formatGenericException($exception);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private static function formatGenericException(Throwable $exception): string
|
|
283
|
-
{
|
|
284
|
-
$type = htmlspecialchars(get_class($exception), ENT_QUOTES, 'UTF-8');
|
|
285
|
-
$message = htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8');
|
|
286
|
-
$file = htmlspecialchars($exception->getFile(), ENT_QUOTES, 'UTF-8');
|
|
287
|
-
$line = $exception->getLine();
|
|
288
|
-
|
|
289
|
-
return <<<HTML
|
|
290
|
-
<div class="error-container max-w-4xl mx-auto mt-8 bg-red-50 border border-red-200 rounded-lg shadow-lg">
|
|
291
|
-
<div class="bg-red-100 px-6 py-4 border-b border-red-200">
|
|
292
|
-
<h2 class="text-xl font-bold text-red-800 flex items-center">
|
|
293
|
-
<svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
294
|
-
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
|
|
295
|
-
</svg>
|
|
296
|
-
{$type}
|
|
297
|
-
</h2>
|
|
298
|
-
</div>
|
|
299
|
-
|
|
300
|
-
<div class="p-6">
|
|
301
|
-
<div class="bg-white border border-red-200 rounded-lg p-4 mb-4">
|
|
302
|
-
<p class="text-red-800 font-medium break-words">{$message}</p>
|
|
303
|
-
</div>
|
|
304
|
-
|
|
305
|
-
<div class="text-sm text-gray-600 mb-4">
|
|
306
|
-
<strong>File:</strong> <code class="bg-gray-100 px-2 py-1 rounded text-xs">{$file}</code><br />
|
|
307
|
-
<strong>Line:</strong> <span class="bg-gray-100 px-2 py-1 rounded text-xs">{$line}</span>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
|
-
<details class="mt-4">
|
|
311
|
-
<summary class="cursor-pointer text-red-600 font-medium hover:text-red-800 select-none">
|
|
312
|
-
Show Stack Trace
|
|
313
|
-
</summary>
|
|
314
|
-
<div class="mt-3 bg-gray-50 border border-gray-200 rounded p-4">
|
|
315
|
-
<pre class="text-xs text-gray-700 overflow-auto whitespace-pre-wrap max-h-96">{$exception->getTraceAsString()}</pre>
|
|
316
|
-
</div>
|
|
317
|
-
</details>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
HTML;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Lib;
|
|
6
|
-
|
|
7
|
-
use RuntimeException;
|
|
8
|
-
use InvalidArgumentException;
|
|
9
|
-
use PPHP\PHPX\TemplateCompiler;
|
|
10
|
-
|
|
11
|
-
class IncludeTracker
|
|
12
|
-
{
|
|
13
|
-
private static array $sections = [];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Includes and echoes a file wrapped in a unique pp-component container.
|
|
17
|
-
* Supported $mode values: 'include', 'include_once', 'require', 'require_once'
|
|
18
|
-
*
|
|
19
|
-
* @param string $filePath The path to the file to be included.
|
|
20
|
-
* @param string $mode The mode of inclusion.
|
|
21
|
-
* @throws RuntimeException If the file does not exist.
|
|
22
|
-
* @throws InvalidArgumentException If an invalid mode is provided.
|
|
23
|
-
* @return void
|
|
24
|
-
*/
|
|
25
|
-
public static function render(string $filePath, string $mode = 'include_once'): void
|
|
26
|
-
{
|
|
27
|
-
if (!file_exists($filePath)) {
|
|
28
|
-
throw new RuntimeException("File not found: $filePath");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
ob_start();
|
|
32
|
-
match ($mode) {
|
|
33
|
-
'include' => include $filePath,
|
|
34
|
-
'include_once' => include_once $filePath,
|
|
35
|
-
'require' => require $filePath,
|
|
36
|
-
'require_once' => require_once $filePath,
|
|
37
|
-
default => throw new InvalidArgumentException("Invalid include mode: $mode"),
|
|
38
|
-
};
|
|
39
|
-
$html = ob_get_clean();
|
|
40
|
-
|
|
41
|
-
$wrapped = self::wrapWithId($filePath, $html);
|
|
42
|
-
$fragDom = TemplateCompiler::convertToXml($wrapped);
|
|
43
|
-
|
|
44
|
-
$newHtml = TemplateCompiler::innerXml($fragDom);
|
|
45
|
-
|
|
46
|
-
self::$sections[$filePath] = [
|
|
47
|
-
'path' => $filePath,
|
|
48
|
-
'html' => $newHtml,
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
echo $newHtml;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private static function wrapWithId(string $filePath, string $html): string
|
|
55
|
-
{
|
|
56
|
-
$id = 's' . base_convert(sprintf('%u', crc32($filePath)), 10, 36);
|
|
57
|
-
return "<div pp-component=\"$id\">\n$html\n</div>";
|
|
58
|
-
}
|
|
59
|
-
}
|