├── .gitignore
├── README.md
├── app
├── database.sqlite
├── index.php
├── source.css
└── views
│ ├── 404.php
│ ├── _layout.php
│ ├── about.php
│ └── home.php
├── public
└── css
│ └── style.css
└── tailwind.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | tailwind
2 | **/logs/*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## PHP no framework HTMX example
2 |
3 | This is a simple example of using HTMX with PHP and no framework. It uses the [HTMX](https://htmx.org/) library to make AJAX requests and update the page with HTML over the wire. It has some basic error handling and exception handling.
4 | We can also parse the HX-request header and render content without template. See `register_shutdown_function()`. In the `/about` page I also included an example of how one can work with a SQLite database.
5 |
6 | I'm also using TailwindCSS, download the CLI (it is not checked in here), place it in `/project` and run it in watch mode with this command:
7 | ```bash
8 | ./tailwind -i ./app/source.css -o ./public/css/style.css --watch
9 | ```
10 |
11 | I'm running this locally on my mac by using https://www.mamp.info/en/mamp/mac/ with a nginx controller that is routing like so:
12 | ```nginx
13 | server {
14 | listen 8999;
15 | server_name simple.local;
16 |
17 | root "/Applications/MAMP/htdocs/project";
18 |
19 | location / {
20 | try_files $uri /app/index.php?$query_string;
21 | }
22 |
23 | location ~ ^/(public/)?.*\.(css|js)$ {
24 | try_files $uri =404;
25 | autoindex on;
26 | }
27 |
28 | location ~ \.php$ {
29 | try_files $uri =404;
30 | fastcgi_pass unix:/Applications/MAMP/Library/logs/fastcgi/nginxFastCGI.sock;
31 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
32 | include fastcgi_params;
33 | }
34 |
35 | location ~ /\. {
36 | deny all;
37 | }
38 | }
39 | ```
40 | Apache/httpd or other web servers should also work. Have fun!
--------------------------------------------------------------------------------
/app/database.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nilskj/php-vanilla-htmx/6888f2ffa3de0ede81cbd7ba65b17541f8f89c0a/app/database.sqlite
--------------------------------------------------------------------------------
/app/index.php:
--------------------------------------------------------------------------------
1 | getMessage() . ' in ' . $throwable->getFile() . ' on line ' . $throwable->getLine() . PHP_EOL;
21 | file_put_contents($logFile, $message, FILE_APPEND);
22 | $content = "An unexpected error occurred. Please check the logs for more information." . PHP_EOL;
23 | }
24 | set_exception_handler('exception_handler');
25 |
26 | function get_sqlite_connection(): ?PDO
27 | {
28 | $sqlite_file = __DIR__ . '/database.sqlite';
29 | try {
30 | $conn = new PDO("sqlite:" . $sqlite_file);
31 | $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
32 | return $conn;
33 | } catch (PDOException $exception) {
34 | exception_handler($exception);
35 | return null;
36 | }
37 | }
38 |
39 | ob_start();
40 | switch ($route) {
41 | case '':
42 | case '/':
43 | require __DIR__ . $viewDir . 'home.php';
44 | break;
45 |
46 | case '/about':
47 | require __DIR__ . $viewDir . 'about.php';
48 | break;
49 |
50 | case '/htmx':
51 | echo '
hello
';
52 | break;
53 |
54 | default:
55 | http_response_code(404);
56 | require __DIR__ . $viewDir . '404.php';
57 | }
58 | $content = ob_get_clean();
--------------------------------------------------------------------------------
/app/source.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/app/views/404.php:
--------------------------------------------------------------------------------
1 | not found 404
--------------------------------------------------------------------------------
/app/views/_layout.php:
--------------------------------------------------------------------------------
1 | no content';
4 | }
5 | ?>
6 |
7 |
8 |
9 |
10 |
11 | My Site
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
--------------------------------------------------------------------------------
/app/views/about.php:
--------------------------------------------------------------------------------
1 | prepare("SELECT * FROM example_table");
5 | $stmt->execute();
6 | $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
7 |
8 | foreach ($result as $row) {
9 | echo $row['id'] . ' - ' . $row['name'] . PHP_EOL;
10 | }
11 | } else {
12 | echo "Failed to establish a connection to the local SQLite database." . PHP_EOL;
13 | }
--------------------------------------------------------------------------------
/app/views/home.php:
--------------------------------------------------------------------------------
1 | hello world!
2 | CLICK ME
3 |
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | /*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./app/**/*.{html,php}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
10 |
--------------------------------------------------------------------------------