create-prisma-php-app 2.0.0-alpha.9 → 2.0.0-beta.10
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/composer.json +1 -1
- package/dist/.htaccess +31 -4
- package/dist/bootstrap.php +0 -1
- package/dist/index.js +40 -50
- package/dist/postcss.config.js +1 -0
- package/dist/src/Lib/AI/ChatGPTClient.php +1 -1
- package/dist/src/Lib/Auth/Auth.php +18 -11
- package/dist/src/Lib/Auth/AuthConfig.php +5 -3
- package/dist/src/Lib/ErrorHandler.php +3 -0
- package/dist/src/Lib/PHPMailer/Mailer.php +47 -6
- package/dist/src/Lib/PHPX/TwMerge.php +14 -0
- package/dist/src/Lib/Request.php +32 -36
- package/dist/src/Lib/StateManager.php +2 -2
- package/dist/src/Lib/Websocket/ConnectionManager.php +4 -2
- package/dist/src/app/css/tailwind.css +24 -5
- package/dist/src/app/error.php +11 -0
- package/dist/src/app/js/index.js +2 -2
- package/package.json +1 -1
- package/dist/prisma/schema.prisma +0 -37
- package/dist/prisma/seed.ts +0 -74
- package/dist/settings/prisma-schema.json +0 -103
- package/dist/settings/prisma-sdk.ts +0 -28
- package/dist/src/Lib/Prisma/Classes/PPHPUtility.php +0 -854
- package/dist/src/Lib/Prisma/Model/IModel.php +0 -24
package/composer.json
CHANGED
package/dist/.htaccess
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Turn on rewrite engine
|
|
2
2
|
RewriteEngine On
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
<
|
|
4
|
+
# Prevent access to sensitive files
|
|
5
|
+
<FilesMatch "(^\.htaccess|\.git|\.env|composer\.(json|lock)|package(-lock)?\.json|phpunit\.xml)$">
|
|
6
6
|
Order allow,deny
|
|
7
7
|
Deny from all
|
|
8
|
-
</
|
|
8
|
+
</FilesMatch>
|
|
9
9
|
|
|
10
10
|
# Allow cross-origin requests (CORS) for all routes
|
|
11
11
|
<IfModule mod_headers.c>
|
|
@@ -32,11 +32,38 @@ RewriteEngine On
|
|
|
32
32
|
</FilesMatch>
|
|
33
33
|
</IfModule>
|
|
34
34
|
|
|
35
|
+
# Add important security headers
|
|
36
|
+
<IfModule mod_headers.c>
|
|
37
|
+
# Enforce HTTPS and prevent protocol downgrade attacks
|
|
38
|
+
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
39
|
+
|
|
40
|
+
# Protect against Cross-Site Scripting (XSS) attacks
|
|
41
|
+
Header set X-XSS-Protection "1; mode=block"
|
|
42
|
+
|
|
43
|
+
# Prevent MIME-type sniffing
|
|
44
|
+
Header set X-Content-Type-Options "nosniff"
|
|
45
|
+
|
|
46
|
+
# Clickjacking protection
|
|
47
|
+
Header always set X-Frame-Options "DENY"
|
|
48
|
+
|
|
49
|
+
# Implement a basic Content Security Policy (CSP)
|
|
50
|
+
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;"
|
|
51
|
+
|
|
52
|
+
# Restrict form submissions
|
|
53
|
+
Header set Content-Security-Policy "form-action 'self'"
|
|
54
|
+
|
|
55
|
+
# Set a strict Referrer Policy
|
|
56
|
+
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
|
57
|
+
|
|
58
|
+
# Control browser permissions (optional but recommended)
|
|
59
|
+
Header set Permissions-Policy "geolocation=(), microphone=(), camera=(), autoplay=()"
|
|
60
|
+
</IfModule>
|
|
61
|
+
|
|
35
62
|
# Exclude static files from being redirected
|
|
36
63
|
RewriteCond %{REQUEST_URI} !\.(css|js|png|jpe?g|gif|svg|webp|woff2?|ttf|eot|ico|pdf|mp4|webm|mp3|ogg)$ [NC]
|
|
37
64
|
RewriteCond %{REQUEST_URI} !^/bootstrap.php
|
|
38
65
|
RewriteRule ^(.*)$ bootstrap.php [QSA,L]
|
|
39
66
|
|
|
40
|
-
#
|
|
67
|
+
# Ensure OPTIONS requests are handled correctly
|
|
41
68
|
RewriteCond %{REQUEST_METHOD} OPTIONS
|
|
42
69
|
RewriteRule ^ - [R=200,L]
|
package/dist/bootstrap.php
CHANGED
|
@@ -627,7 +627,6 @@ final class Bootstrap
|
|
|
627
627
|
|
|
628
628
|
public static function getLoadingsFiles(): string
|
|
629
629
|
{
|
|
630
|
-
// Gather all loading.php files
|
|
631
630
|
$loadingFiles = array_filter(PrismaPHPSettings::$routeFiles, function ($route) {
|
|
632
631
|
$normalizedRoute = str_replace('\\', '/', $route);
|
|
633
632
|
return preg_match('/\/loading\.php$/', $normalizedRoute);
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{execSync}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"],dockerFiles=[".dockerignore","docker-compose.yml","Dockerfile","apache.conf"];function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let c=`http://localhost/${n}`;c=c.endsWith("/")?c.slice(0,-1):c;const i=c.replace(/(?<!:)(\/\/+)/g,"/"),o=n.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 n=JSON.parse(fs.readFileSync(t,"utf8"));n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let c=[];if(s.tailwindcss&&(n.scripts={...n.scripts,tailwind:"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch"},c.push("tailwind")),s.websocket&&(n.scripts={...n.scripts,websocket:"tsx settings/restart-websocket.ts"},c.push("websocket")),s.docker&&(n.scripts={...n.scripts,docker:"docker-compose up"},c.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";n.scripts={...n.scripts,"create-swagger-docs":e}}let i={...n.scripts};i.browserSync="tsx settings/bs-config.ts",i.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`,n.scripts=i,n.type="module",s.prisma&&(n.prisma={seed:"tsx prisma/seed.ts"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e,s){const t=path.join(e,"composer.json");if(checkExcludeFiles(t))return;let n;if(fs.existsSync(t)){{const e=fs.readFileSync(t,"utf8");n=JSON.parse(e)}s.websocket&&(n.require={...n.require,"cboden/ratchet":"^0.4.4"}),s.prisma&&(n.require={...n.require,"calicastle/cuid":"^2.0.0"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let n=fs.readFileSync(t,"utf8");n+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,n,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateLocalStoreKey(){return randomBytes(16).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),c=n&&fs.statSync(e);if(n&&c&&c.isDirectory()){const n=s.toLowerCase();if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(!t.prisma&&n.includes("src\\lib\\prisma"))return;if(t.backendOnly&&n.includes("src\\app\\js")||t.backendOnly&&n.includes("src\\app\\css")||t.backendOnly&&n.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&n.includes("src\\app\\swagger-docs"))return;const c=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),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")||s.includes("restart-websocket.bat")))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.prisma&&(s.includes("prisma-sdk.ts")||s.includes("prisma-schema.json")))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:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),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 },\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"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),n+='\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"><\/script>');let c="";s.backendOnly||(c=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${n}`:n),e=e.replace("</head>",`${c}\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:"/../composer.json",dest:"/composer.json"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.prisma&&n.push({src:"/prisma",dest:"/prisma"}),s.docker&&n.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 n=path.join(__dirname,s),c=path.join(e,t);if(checkExcludeFiles(c))return;const i=fs.readFileSync(n,"utf8");fs.writeFileSync(c,i,{flag:"w"})})),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e,s),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const c=`# Prisma PHP Auth Secret Key For development only - Change this in production\nAUTH_SECRET="${generateAuthSecret()}"\n\n# PHPMailer\n# SMTP_HOST="smtp.gmail.com" or your SMTP host\n# SMTP_USERNAME="john.doe@gmail.com" or your SMTP username\n# SMTP_PASSWORD="123456"\n# SMTP_PORT="587" for TLS, 465 for SSL or your SMTP port\n# SMTP_ENCRYPTION="ssl" or tls\n# MAIL_FROM="john.doe@gmail.com"\n# MAIL_FROM_NAME="John Doe"\n\n# SHOW ERRORS - Set to true to show errors in the browser for development only - Change this in production to false\nSHOW_ERRORS="true"\n\n# APP TIMEZONE - Set your application timezone - Default is "UTC"\nAPP_TIMEZONE="UTC"\n\n# APP ENV - Set your application environment - Default is "development" - Change this in production to "production"\nAPP_ENV="development"\n\n# APP CACHE ENABLED - Set to true to enable caching - Default is false\nCACHE_ENABLED="false"\n# APP CACHE TTL - Set the cache time to live in seconds - Default is 600 seconds (10 minutes)\nCACHE_TTL="600"\n\n# LOCAL STORAGE KEY - Define a custom key for local storage.\n# If not set, it defaults to "pphp_local_store_59e13".\n# Spaces in the value will be replaced with underscores, and the key will be converted to lowercase automatically.\nLOCALSTORE_KEY="${generateLocalStoreKey()}"`;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${c}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,c)}async function getAnswer(e={}){const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||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)},n=await prompts(s,{onCancel:t}),c=[];n.backendOnly||e.backendOnly?(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!0,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!0,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const i=await prompts(c,{onCancel:t});return{projectName:n.projectName?String(n.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:n.backendOnly??e.backendOnly??!1,swaggerDocs:i.swaggerDocs??e.swaggerDocs??!1,tailwindcss:i.tailwindcss??e.tailwindcss??!1,websocket:i.websocket??e.websocket??!1,prisma:i.prisma??e.prisma??!1,docker:i.docker??e.docker??!1}}async function uninstallDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let n="";e.on("data",(e=>n+=e)),e.on("end",(()=>{try{const e=JSON.parse(n);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),n=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>n[e])return 1;if(t[e]<n[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}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"];function bsConfigUrls(e){const s=e.indexOf("\\htdocs\\");if(-1===s)return{bsTarget:"",bsPathRewrite:{}};const t=e.substring(0,s+"\\htdocs\\".length).replace(/\\/g,"\\\\"),n=e.replace(new RegExp(`^${t}`),"").replace(/\\/g,"/");let c=`http://localhost/${n}`;c=c.endsWith("/")?c.slice(0,-1):c;const o=c.replace(/(?<!:)(\/\/+)/g,"/"),i=n.replace(/\/\/+/g,"/");return{bsTarget:`${o}/`,bsPathRewrite:{"^/":`/${i.startsWith("/")?i.substring(1):i}/`}}}async function updatePackageJson(e,s){const t=path.join(e,"package.json");if(checkExcludeFiles(t))return;const n=JSON.parse(fs.readFileSync(t,"utf8"));n.scripts={...n.scripts,projectName:"tsx settings/project-name.ts"};let c=[];if(s.tailwindcss&&(n.scripts={...n.scripts,tailwind:"postcss src/app/css/tailwind.css -o src/app/css/styles.css --watch"},c.push("tailwind")),s.websocket&&(n.scripts={...n.scripts,websocket:"tsx settings/restart-websocket.ts"},c.push("websocket")),s.docker&&(n.scripts={...n.scripts,docker:"docker-compose up"},c.push("docker")),s.swaggerDocs){const e=s.prisma?"tsx settings/auto-swagger-docs.ts":"tsx settings/swagger-config.ts";n.scripts={...n.scripts,"create-swagger-docs":e}}let o={...n.scripts};o.browserSync="tsx settings/bs-config.ts",o.dev=`npm-run-all projectName -p browserSync ${c.join(" ")}`,n.scripts=o,n.type="module",s.prisma&&(n.prisma={seed:"tsx prisma/seed.ts"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}async function updateComposerJson(e,s){const t=path.join(e,"composer.json");if(checkExcludeFiles(t))return;let n;if(fs.existsSync(t)){{const e=fs.readFileSync(t,"utf8");n=JSON.parse(e)}s.websocket&&(n.require={...n.require,"cboden/ratchet":"^0.4.4"}),s.prisma&&(n.require={...n.require,"calicastle/cuid":"^2.0.0"}),fs.writeFileSync(t,JSON.stringify(n,null,2))}}async function updateIndexJsForWebSocket(e,s){if(!s.websocket)return;const t=path.join(e,"src","app","js","index.js");if(checkExcludeFiles(t))return;let n=fs.readFileSync(t,"utf8");n+='\n// WebSocket initialization\nvar ws = new WebSocket("ws://localhost:8080");\n',fs.writeFileSync(t,n,"utf8")}function generateAuthSecret(){return randomBytes(33).toString("base64")}function generateLocalStoreKey(){return randomBytes(16).toString("hex")}function copyRecursiveSync(e,s,t){const n=fs.existsSync(e),c=n&&fs.statSync(e);if(n&&c&&c.isDirectory()){const n=s.toLowerCase();if(!t.websocket&&n.includes("src\\lib\\websocket"))return;if(t.backendOnly&&n.includes("src\\app\\js")||t.backendOnly&&n.includes("src\\app\\css")||t.backendOnly&&n.includes("src\\app\\assets"))return;if(!t.swaggerDocs&&n.includes("src\\app\\swagger-docs"))return;const c=s.replace(/\\/g,"/");if(updateAnswer?.excludeFilePath?.includes(c))return;fs.existsSync(s)||fs.mkdirSync(s,{recursive:!0}),fs.readdirSync(e).forEach((n=>{copyRecursiveSync(path.join(e,n),path.join(s,n),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")||s.includes("restart-websocket.bat")))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:n})=>{copyRecursiveSync(path.join(__dirname,s),path.join(e,n),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"),n="";s.backendOnly||(s.tailwindcss||(n='\n <link href="<?= Request::baseUrl; ?>/css/index.css" rel="stylesheet" />'),n+='\n <script src="<?= Request::baseUrl; ?>/js/json5.min.js"><\/script>\n <script src="<?= Request::baseUrl; ?>/js/index.js"><\/script>');let c="";s.backendOnly||(c=s.tailwindcss?` <link href="<?= Request::baseUrl; ?>/css/styles.css" rel="stylesheet" /> ${n}`:n),e=e.replace("</head>",`${c}\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:"/../composer.json",dest:"/composer.json"},{src:"/tsconfig.json",dest:"/tsconfig.json"},{src:"/app-gitignore",dest:"/.gitignore"}];s.tailwindcss&&t.push({src:"/postcss.config.js",dest:"/postcss.config.js"});const n=[{src:"/settings",dest:"/settings"},{src:"/src",dest:"/src"}];s.docker&&n.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 n=path.join(__dirname,s),c=path.join(e,t);if(checkExcludeFiles(c))return;const o=fs.readFileSync(n,"utf8");fs.writeFileSync(c,o,{flag:"w"})})),await executeCopy(e,n,s),await updatePackageJson(e,s),await updateComposerJson(e,s),s.backendOnly||await updateIndexJsForWebSocket(e,s),s.tailwindcss&&modifyPostcssConfig(e),(s.tailwindcss||!s.backendOnly||s.swaggerDocs)&&modifyLayoutPHP(e,s);const c=`# Prisma PHP Auth Secret Key\nAUTH_SECRET="${generateAuthSecret()}"\n\n# PHPMailer\n# SMTP_HOST="smtp.gmail.com" or your SMTP host\n# SMTP_USERNAME="john.doe@gmail.com" or your SMTP username\n# SMTP_PASSWORD="123456"\n# SMTP_PORT="587" for TLS, 465 for SSL or your SMTP port\n# SMTP_ENCRYPTION="ssl" or tls\n# MAIL_FROM="john.doe@gmail.com"\n# MAIL_FROM_NAME="John Doe"\n\n# SHOW ERRORS - Set to true to show errors in the browser for development only - Change this in production to false\nSHOW_ERRORS="true"\n\n# APP TIMEZONE - Set your application timezone - Default is "UTC"\nAPP_TIMEZONE="UTC"\n\n# APP ENV - Set your application environment - Default is "development" - Change this in production to "production"\nAPP_ENV="development"\n\n# APP CACHE ENABLED - Set to true to enable caching - Default is false\nCACHE_ENABLED="false"\n# APP CACHE TTL - Set the cache time to live in seconds - Default is 600 seconds (10 minutes)\nCACHE_TTL="600"\n\n# LOCAL STORAGE KEY - Define a custom key for local storage.\n# If not set, it defaults to "pphp_local_store_59e13".\n# Spaces in the value will be replaced with underscores, and the key will be converted to lowercase automatically.\nLOCALSTORE_KEY="${generateLocalStoreKey()}"`;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${c}`;await createOrUpdateEnvFile(e,s)}else await createOrUpdateEnvFile(e,c)}async function getAnswer(e={}){const s=[];e.projectName||s.push({type:"text",name:"projectName",message:"What is your project named?",initial:"my-app"}),e.backendOnly||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)},n=await prompts(s,{onCancel:t}),c=[];n.backendOnly||e.backendOnly?(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!0,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!0,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"})):(e.swaggerDocs||c.push({type:"toggle",name:"swaggerDocs",message:`Would you like to use ${chalk.blue("Swagger Docs")}?`,initial:!1,active:"Yes",inactive:"No"}),e.tailwindcss||c.push({type:"toggle",name:"tailwindcss",message:`Would you like to use ${chalk.blue("Tailwind CSS")}?`,initial:!1,active:"Yes",inactive:"No"}),e.websocket||c.push({type:"toggle",name:"websocket",message:`Would you like to use ${chalk.blue("Websocket")}?`,initial:!1,active:"Yes",inactive:"No"}),e.prisma||c.push({type:"toggle",name:"prisma",message:`Would you like to use ${chalk.blue("Prisma PHP ORM")}?`,initial:!1,active:"Yes",inactive:"No"}),e.docker||c.push({type:"toggle",name:"docker",message:`Would you like to use ${chalk.blue("Docker")}?`,initial:!1,active:"Yes",inactive:"No"}));const o=await prompts(c,{onCancel:t});return{projectName:n.projectName?String(n.projectName).trim().replace(/ /g,"-"):e.projectName??"my-app",backendOnly:n.backendOnly??e.backendOnly??!1,swaggerDocs:o.swaggerDocs??e.swaggerDocs??!1,tailwindcss:o.tailwindcss??e.tailwindcss??!1,websocket:o.websocket??e.websocket??!1,prisma:o.prisma??e.prisma??!1,docker:o.docker??e.docker??!1}}async function uninstallDependencies(e,s,t=!1){s.forEach((e=>{}));const n=`npm uninstall ${t?"--save-dev":"--save"} ${s.join(" ")}`;execSync(n,{stdio:"inherit",cwd:e})}function fetchPackageVersion(e){return new Promise(((s,t)=>{https.get(`https://registry.npmjs.org/${e}`,(e=>{let n="";e.on("data",(e=>n+=e)),e.on("end",(()=>{try{const e=JSON.parse(n);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),n=s.split(".").map(Number);for(let e=0;e<t.length;e++){if(t[e]>n[e])return 1;if(t[e]<n[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.
|
|
@@ -32,26 +32,23 @@ async function installDependencies(baseDir, dependencies, isDev = false) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
const pinnedVersions = {
|
|
35
|
-
"@
|
|
36
|
-
"@prisma/internals": "^6.4.0",
|
|
37
|
-
"@tailwindcss/postcss": "^4.0.7",
|
|
35
|
+
"@tailwindcss/postcss": "^4.0.12",
|
|
38
36
|
"@types/browser-sync": "^2.29.0",
|
|
39
|
-
"@types/node": "^22.13.
|
|
37
|
+
"@types/node": "^22.13.10",
|
|
40
38
|
"@types/prompts": "^2.4.9",
|
|
41
|
-
autoprefixer: "^10.4.20",
|
|
42
39
|
"browser-sync": "^3.0.3",
|
|
43
40
|
chalk: "^5.4.1",
|
|
44
41
|
"chokidar-cli": "^3.0.0",
|
|
42
|
+
cssnano: "^7.0.6",
|
|
45
43
|
"http-proxy-middleware": "^3.0.3",
|
|
46
44
|
"npm-run-all": "^4.1.5",
|
|
47
45
|
"php-parser": "^3.2.2",
|
|
48
|
-
postcss: "^8.5.
|
|
46
|
+
postcss: "^8.5.3",
|
|
49
47
|
"postcss-cli": "^11.0.0",
|
|
50
|
-
prisma: "^6.4.0",
|
|
51
48
|
prompts: "^2.4.2",
|
|
52
|
-
tailwindcss: "^4.0.
|
|
49
|
+
tailwindcss: "^4.0.12",
|
|
53
50
|
tsx: "^4.19.3",
|
|
54
|
-
typescript: "^5.
|
|
51
|
+
typescript: "^5.8.2",
|
|
55
52
|
};
|
|
56
53
|
function pkg(name) {
|
|
57
54
|
return pinnedVersions[name] ? `${name}@${pinnedVersions[name]}` : name;
|
|
@@ -84,26 +81,28 @@ async function main() {
|
|
|
84
81
|
}
|
|
85
82
|
const currentDir = process.cwd();
|
|
86
83
|
const configPath = path.join(currentDir, "prisma-php.json");
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
84
|
+
if (fs.existsSync(configPath)) {
|
|
85
|
+
const localSettings = readJsonFile(configPath);
|
|
86
|
+
let excludeFiles = [];
|
|
87
|
+
localSettings.excludeFiles?.map((file) => {
|
|
88
|
+
const filePath = path.join(currentDir, file);
|
|
89
|
+
if (fs.existsSync(filePath))
|
|
90
|
+
excludeFiles.push(filePath.replace(/\\/g, "/"));
|
|
91
|
+
});
|
|
92
|
+
updateAnswer = {
|
|
93
|
+
projectName,
|
|
94
|
+
backendOnly: answer?.backendOnly ?? false,
|
|
95
|
+
swaggerDocs: answer?.swaggerDocs ?? false,
|
|
96
|
+
tailwindcss: answer?.tailwindcss ?? false,
|
|
97
|
+
websocket: answer?.websocket ?? false,
|
|
98
|
+
prisma: answer?.prisma ?? false,
|
|
99
|
+
docker: answer?.docker ?? false,
|
|
100
|
+
isUpdate: true,
|
|
101
|
+
excludeFiles: localSettings.excludeFiles ?? [],
|
|
102
|
+
excludeFilePath: excludeFiles ?? [],
|
|
103
|
+
filePath: currentDir,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
107
106
|
} else {
|
|
108
107
|
answer = await getAnswer();
|
|
109
108
|
}
|
|
@@ -124,10 +123,10 @@ async function main() {
|
|
|
124
123
|
latestVersionOfCreatePrismaPhpApp
|
|
125
124
|
) === -1
|
|
126
125
|
) {
|
|
127
|
-
execSync(
|
|
126
|
+
execSync("npm uninstall -g create-prisma-php-app", {
|
|
128
127
|
stdio: "inherit",
|
|
129
128
|
});
|
|
130
|
-
execSync(
|
|
129
|
+
execSync("npm install -g create-prisma-php-app", {
|
|
131
130
|
stdio: "inherit",
|
|
132
131
|
});
|
|
133
132
|
}
|
|
@@ -156,40 +155,31 @@ async function main() {
|
|
|
156
155
|
dependencies.push(pkg("swagger-jsdoc"), pkg("@types/swagger-jsdoc"));
|
|
157
156
|
}
|
|
158
157
|
if (answer.swaggerDocs && answer.prisma) {
|
|
159
|
-
dependencies.push(
|
|
160
|
-
pkg("prompts"),
|
|
161
|
-
pkg("@types/prompts"),
|
|
162
|
-
pkg("@prisma/internals")
|
|
163
|
-
);
|
|
158
|
+
dependencies.push(pkg("prompts"), pkg("@types/prompts"));
|
|
164
159
|
}
|
|
165
160
|
if (answer.tailwindcss) {
|
|
166
161
|
dependencies.push(
|
|
167
162
|
pkg("tailwindcss"),
|
|
168
163
|
pkg("postcss"),
|
|
169
164
|
pkg("postcss-cli"),
|
|
170
|
-
pkg("@tailwindcss/postcss")
|
|
165
|
+
pkg("@tailwindcss/postcss"),
|
|
166
|
+
pkg("cssnano")
|
|
171
167
|
);
|
|
172
168
|
}
|
|
173
169
|
if (answer.websocket) {
|
|
174
170
|
dependencies.push(pkg("chokidar-cli"));
|
|
175
171
|
}
|
|
176
172
|
if (answer.prisma) {
|
|
177
|
-
dependencies.push(
|
|
178
|
-
pkg("prisma"),
|
|
179
|
-
pkg("@prisma/client"),
|
|
180
|
-
pkg("@prisma/internals")
|
|
181
|
-
);
|
|
182
173
|
execSync("npm install -g prisma-client-php", { stdio: "inherit" });
|
|
183
174
|
}
|
|
184
175
|
await installDependencies(projectPath, dependencies, true);
|
|
185
176
|
if (!projectName) {
|
|
186
|
-
execSync(
|
|
177
|
+
execSync("npx tsc --init", { stdio: "inherit" });
|
|
187
178
|
}
|
|
179
|
+
await createDirectoryStructure(projectPath, answer);
|
|
188
180
|
if (answer.prisma) {
|
|
189
|
-
|
|
190
|
-
execSync(`npx prisma init`, { stdio: "inherit" });
|
|
181
|
+
execSync("npx ppo init --prisma-php", { stdio: "inherit" });
|
|
191
182
|
}
|
|
192
|
-
await createDirectoryStructure(projectPath, answer);
|
|
193
183
|
if (answer.swaggerDocs) {
|
|
194
184
|
const swaggerDocsPath = path.join(
|
|
195
185
|
projectPath,
|
|
@@ -264,8 +254,7 @@ async function main() {
|
|
|
264
254
|
"swagger-jsdoc",
|
|
265
255
|
"@types/swagger-jsdoc",
|
|
266
256
|
"prompts",
|
|
267
|
-
"@types/prompts"
|
|
268
|
-
"@prisma/internals"
|
|
257
|
+
"@types/prompts"
|
|
269
258
|
);
|
|
270
259
|
}
|
|
271
260
|
if (!updateAnswer.tailwindcss) {
|
|
@@ -283,7 +272,8 @@ async function main() {
|
|
|
283
272
|
"tailwindcss",
|
|
284
273
|
"postcss",
|
|
285
274
|
"postcss-cli",
|
|
286
|
-
"@tailwindcss/postcss"
|
|
275
|
+
"@tailwindcss/postcss",
|
|
276
|
+
"cssnano"
|
|
287
277
|
);
|
|
288
278
|
}
|
|
289
279
|
if (!updateAnswer.websocket) {
|
package/dist/postcss.config.js
CHANGED
|
@@ -16,7 +16,7 @@ class ChatGPTClient
|
|
|
16
16
|
private string $apiKey = '';
|
|
17
17
|
private array $cache = [];
|
|
18
18
|
|
|
19
|
-
public function __construct(Client $client = null)
|
|
19
|
+
public function __construct(?Client $client = null)
|
|
20
20
|
{
|
|
21
21
|
// Initialize the Guzzle HTTP client, allowing for dependency injection
|
|
22
22
|
$this->client = $client ?: new Client();
|
|
@@ -12,6 +12,9 @@ use Lib\Validator;
|
|
|
12
12
|
use GuzzleHttp\Client;
|
|
13
13
|
use GuzzleHttp\Exception\RequestException;
|
|
14
14
|
use Lib\Request;
|
|
15
|
+
use Exception;
|
|
16
|
+
use InvalidArgumentException;
|
|
17
|
+
use ArrayObject;
|
|
15
18
|
|
|
16
19
|
class Auth
|
|
17
20
|
{
|
|
@@ -70,14 +73,14 @@ class Auth
|
|
|
70
73
|
* try {
|
|
71
74
|
* $jwt = $auth->signIn('Admin', '1h');
|
|
72
75
|
* echo "JWT: " . $jwt;
|
|
73
|
-
* } catch (
|
|
76
|
+
* } catch (InvalidArgumentException $e) {
|
|
74
77
|
* echo "Error: " . $e->getMessage();
|
|
75
78
|
* }
|
|
76
79
|
*/
|
|
77
|
-
public function signIn($data, string $tokenValidity = null): string
|
|
80
|
+
public function signIn($data, ?string $tokenValidity = null): string
|
|
78
81
|
{
|
|
79
82
|
if (!$this->secretKey) {
|
|
80
|
-
throw new
|
|
83
|
+
throw new InvalidArgumentException("Secret key is required for authentication.");
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
$expirationTime = $this->calculateExpirationTime($tokenValidity ?? $this->defaultTokenValidity);
|
|
@@ -131,6 +134,10 @@ class Auth
|
|
|
131
134
|
return false;
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
if (!isset($_SESSION[self::PAYLOAD_SESSION_KEY])) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
return true;
|
|
135
142
|
}
|
|
136
143
|
|
|
@@ -158,11 +165,11 @@ class Auth
|
|
|
158
165
|
case 'd':
|
|
159
166
|
return new DateInterval("P{$value}D");
|
|
160
167
|
default:
|
|
161
|
-
throw new
|
|
168
|
+
throw new InvalidArgumentException("Invalid duration format: {$duration}");
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
throw new
|
|
172
|
+
throw new InvalidArgumentException("Invalid duration format: {$duration}");
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
/**
|
|
@@ -190,7 +197,7 @@ class Auth
|
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
return $token;
|
|
193
|
-
} catch (
|
|
200
|
+
} catch (Exception) {
|
|
194
201
|
return null;
|
|
195
202
|
}
|
|
196
203
|
}
|
|
@@ -210,12 +217,12 @@ class Auth
|
|
|
210
217
|
*
|
|
211
218
|
* @throws InvalidArgumentException Thrown if the token is invalid.
|
|
212
219
|
*/
|
|
213
|
-
public function refreshToken(string $jwt, string $tokenValidity = null): string
|
|
220
|
+
public function refreshToken(string $jwt, ?string $tokenValidity = null): string
|
|
214
221
|
{
|
|
215
222
|
$decodedToken = $this->verifyToken($jwt);
|
|
216
223
|
|
|
217
224
|
if (!$decodedToken) {
|
|
218
|
-
throw new
|
|
225
|
+
throw new InvalidArgumentException("Invalid token.");
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
$expirationTime = $this->calculateExpirationTime($tokenValidity ?? $this->defaultTokenValidity);
|
|
@@ -256,7 +263,7 @@ class Auth
|
|
|
256
263
|
*
|
|
257
264
|
* @return void
|
|
258
265
|
*/
|
|
259
|
-
public function signOut(string $redirect = null)
|
|
266
|
+
public function signOut(?string $redirect = null)
|
|
260
267
|
{
|
|
261
268
|
if (isset($_COOKIE[self::COOKIE_NAME])) {
|
|
262
269
|
unset($_COOKIE[self::COOKIE_NAME]);
|
|
@@ -282,7 +289,7 @@ class Auth
|
|
|
282
289
|
{
|
|
283
290
|
if (isset($_SESSION[self::PAYLOAD_SESSION_KEY])) {
|
|
284
291
|
$value = $_SESSION[self::PAYLOAD_SESSION_KEY][self::PAYLOAD_NAME];
|
|
285
|
-
return is_array($value) ? new
|
|
292
|
+
return is_array($value) ? new ArrayObject($value, ArrayObject::ARRAY_AS_PROPS) : $value;
|
|
286
293
|
}
|
|
287
294
|
|
|
288
295
|
return null;
|
|
@@ -317,7 +324,7 @@ class Auth
|
|
|
317
324
|
private function findProvider(array $providers, string $type): ?object
|
|
318
325
|
{
|
|
319
326
|
foreach ($providers as $provider) {
|
|
320
|
-
if ($provider
|
|
327
|
+
if (is_object($provider) && get_class($provider) === $type) {
|
|
321
328
|
return $provider;
|
|
322
329
|
}
|
|
323
330
|
}
|
|
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Lib\Auth;
|
|
6
6
|
|
|
7
|
+
use ArrayObject;
|
|
8
|
+
|
|
7
9
|
enum AuthRole: string
|
|
8
10
|
{
|
|
9
11
|
case Admin = 'Admin';
|
|
@@ -67,13 +69,13 @@ final class AuthConfig
|
|
|
67
69
|
/**
|
|
68
70
|
* Checks if the given user role is authorized to access a set of roles.
|
|
69
71
|
*
|
|
70
|
-
* @param
|
|
72
|
+
* @param ArrayObject|string $userRole The user's role to check.
|
|
71
73
|
* @param array<AuthRole> $roles An array of AuthRole instances specifying allowed roles.
|
|
72
74
|
* @return bool Returns true if the user's role matches any of the allowed roles, false otherwise.
|
|
73
75
|
*/
|
|
74
|
-
public static function checkAuthRole(
|
|
76
|
+
public static function checkAuthRole(ArrayObject|string $userRole, array $roles): bool
|
|
75
77
|
{
|
|
76
|
-
if ($userRole instanceof
|
|
78
|
+
if ($userRole instanceof ArrayObject) {
|
|
77
79
|
$userRole = $userRole[Auth::ROLE_NAME] ?? '';
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -38,7 +38,8 @@ class Mailer
|
|
|
38
38
|
* @param string $to The recipient's email address.
|
|
39
39
|
* @param string $subject The subject of the email.
|
|
40
40
|
* @param string $body The HTML body of the email.
|
|
41
|
-
* @param array $options (optional) Additional email options like name, altBody, CC, and
|
|
41
|
+
* @param array $options (optional) Additional email options like name, altBody, CC, BCC, and attachments.
|
|
42
|
+
* - attachments: A string or an array of file paths, or an array of associative arrays with keys 'path' and 'name'.
|
|
42
43
|
*
|
|
43
44
|
* @return bool Returns true if the email is sent successfully, false otherwise.
|
|
44
45
|
*
|
|
@@ -50,7 +51,7 @@ class Mailer
|
|
|
50
51
|
// Validate and sanitize inputs
|
|
51
52
|
$to = Validator::email($to);
|
|
52
53
|
if (!$to) {
|
|
53
|
-
throw new
|
|
54
|
+
throw new Exception('Invalid email address for the main recipient');
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
$subject = Validator::string($subject);
|
|
@@ -60,6 +61,7 @@ class Mailer
|
|
|
60
61
|
$name = $options['name'] ?? '';
|
|
61
62
|
$addCC = $options['addCC'] ?? [];
|
|
62
63
|
$addBCC = $options['addBCC'] ?? [];
|
|
64
|
+
$attachments = $options['attachments'] ?? [];
|
|
63
65
|
|
|
64
66
|
$name = Validator::string($name);
|
|
65
67
|
|
|
@@ -67,6 +69,10 @@ class Mailer
|
|
|
67
69
|
$this->handleRecipients($addCC, 'CC');
|
|
68
70
|
// Handle BCC recipients
|
|
69
71
|
$this->handleRecipients($addBCC, 'BCC');
|
|
72
|
+
// Handle file attachments if provided
|
|
73
|
+
if (!empty($attachments)) {
|
|
74
|
+
$this->handleAttachments($attachments);
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
// Set the main recipient and other email properties
|
|
72
78
|
$this->mail->addAddress($to, $name);
|
|
@@ -77,8 +83,8 @@ class Mailer
|
|
|
77
83
|
|
|
78
84
|
// Send the email
|
|
79
85
|
return $this->mail->send();
|
|
80
|
-
} catch (
|
|
81
|
-
throw new
|
|
86
|
+
} catch (Exception $e) {
|
|
87
|
+
throw new Exception($e->getMessage());
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -101,7 +107,7 @@ class Mailer
|
|
|
101
107
|
if ($recipient) {
|
|
102
108
|
$this->mail->{$method}($recipient);
|
|
103
109
|
} else {
|
|
104
|
-
throw new
|
|
110
|
+
throw new Exception("Invalid email address in $type");
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
} else {
|
|
@@ -109,12 +115,47 @@ class Mailer
|
|
|
109
115
|
if ($recipient) {
|
|
110
116
|
$this->mail->{$method}($recipient);
|
|
111
117
|
} else {
|
|
112
|
-
throw new
|
|
118
|
+
throw new Exception("Invalid email address in $type");
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Handle adding file attachments.
|
|
126
|
+
*
|
|
127
|
+
* @param string|array $attachments File path(s) to attach.
|
|
128
|
+
* You can pass a string for a single file or an array of file paths.
|
|
129
|
+
* Alternatively, each attachment can be an array with keys 'path' and 'name' for custom naming.
|
|
130
|
+
*
|
|
131
|
+
* @throws Exception Throws an exception if any attachment file is not found.
|
|
132
|
+
*/
|
|
133
|
+
private function handleAttachments(string|array $attachments): void
|
|
134
|
+
{
|
|
135
|
+
if (is_array($attachments)) {
|
|
136
|
+
foreach ($attachments as $attachment) {
|
|
137
|
+
if (is_array($attachment)) {
|
|
138
|
+
$file = $attachment['path'] ?? null;
|
|
139
|
+
$name = $attachment['name'] ?? '';
|
|
140
|
+
if (!$file || !file_exists($file)) {
|
|
141
|
+
throw new Exception("Attachment file does not exist: " . ($file ?? 'unknown'));
|
|
142
|
+
}
|
|
143
|
+
$this->mail->addAttachment($file, $name);
|
|
144
|
+
} else {
|
|
145
|
+
if (!file_exists($attachment)) {
|
|
146
|
+
throw new Exception("Attachment file does not exist: $attachment");
|
|
147
|
+
}
|
|
148
|
+
$this->mail->addAttachment($attachment);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
if (!file_exists($attachments)) {
|
|
153
|
+
throw new Exception("Attachment file does not exist: $attachments");
|
|
154
|
+
}
|
|
155
|
+
$this->mail->addAttachment($attachments);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
118
159
|
/**
|
|
119
160
|
* Convert HTML content to plain text.
|
|
120
161
|
*
|
|
@@ -167,6 +167,20 @@ class TwMerge
|
|
|
167
167
|
$splitClasses = preg_split("/\s+/", $item);
|
|
168
168
|
foreach ($splitClasses as $individualClass) {
|
|
169
169
|
$classKey = self::getClassGroup($individualClass);
|
|
170
|
+
|
|
171
|
+
// If the class is non-responsive (no colon), remove any responsive variants for the same base
|
|
172
|
+
if (strpos($classKey, ':') === false) {
|
|
173
|
+
// The base group is the class key itself (e.g. "justify")
|
|
174
|
+
$baseGroup = $classKey;
|
|
175
|
+
// Remove any entries that end with the same base group but have a prefix (e.g. "sm:justify")
|
|
176
|
+
foreach ($classArray as $existingKey => $existingClass) {
|
|
177
|
+
if ($existingKey !== $baseGroup && substr($existingKey, -strlen($baseGroup)) === $baseGroup) {
|
|
178
|
+
unset($classArray[$existingKey]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Get the conflicting keys and remove them
|
|
170
184
|
$conflictingKeys = self::getConflictingKeys($classKey);
|
|
171
185
|
|
|
172
186
|
// Remove any conflicting classes
|