Repository navigation
Expand file tree
/
Copy pathmatrix
More file actions
executable file
·3449 lines (3008 loc) · 114 KB
/
Copy pathmatrix
File metadata and controls
executable file
·3449 lines (3008 loc) · 114 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
# SiteMatrix - Unified PHP Stack Management Script
set -euo pipefail
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR"
# Detect if running from frontend/ subdirectory and adjust paths
if [[ "$(basename "$SCRIPT_DIR")" == "frontend" ]]; then
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
fi
DOMAIN_SUFFIX="127.0.0.1.nip.io"
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.yml"
DOCKER_COMPOSE="docker-compose" # Default fallback
# DEFAULT_PHP_VERSION now sourced from config/validation.sh
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Detect host LAN IP for user-facing URLs so other devices on the network can connect.
# get_lan_ip / HOST_IP live in scripts/helpers.sh (shared with scripts/*.sh subprocesses).
JSON_OUTPUT=false
output_json() {
echo "$1"
}
error_json() {
local code="${1:-}"
local message="$2"
printf '{"success":false,"error":{"code":"%s","message":"%s"}}\n' "$code" "$message"
exit 1
}
check_docker_json() {
if [[ "$JSON_OUTPUT" == true ]]; then
if ! $CONTAINER_RUNTIME info >/dev/null 2>&1; then
error_json "DOCKER_ERROR" "Docker daemon is not running"
fi
fi
}
# sanitize_sql_identifier, validate_site_name now sourced from config/validation.sh
# Helper functions
log_info() { echo -e "${BLUE}ℹ️ $1${NC}"; }
log_success() { echo -e "${GREEN}✅ $1${NC}"; }
log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
log_error() { echo -e "${RED}❌ $1${NC}"; }
json_log_override() {
if [[ "$JSON_OUTPUT" == true ]]; then
log_info() { :; }
log_success() { :; }
log_warning() { :; }
log_error() { :; }
fi
}
ACTIVITY_LOG="$PROJECT_ROOT/logs/activity.log"
log_activity() {
local action="${1:-}"
local site="${2:-}"
local details="${3:-}"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $action | $site | $details" >> "$ACTIVITY_LOG"
}
# Load environment
if [[ -f "$PROJECT_ROOT/.env" ]]; then
set -a
source "$PROJECT_ROOT/.env"
set +a
fi
# Use podman-compose instead of docker-compose
# Preset DOCKER_COMPOSE wins (tests/CI inject the stub; without this guard
# sourcing matrix on a compose-less host aborts — same rule as common.sh).
if [[ -z "${DOCKER_COMPOSE:-}" ]]; then
if command -v podman-compose >/dev/null 2>&1; then
DOCKER_COMPOSE="podman-compose"
elif command -v docker-compose >/dev/null 2>&1; then
DOCKER_COMPOSE="docker-compose"
else
log_error "Neither docker-compose nor podman-compose found"
exit 1
fi
fi
# Derive container runtime from compose tool
if [[ "$DOCKER_COMPOSE" == "podman-compose" ]]; then
CONTAINER_RUNTIME="podman"
else
CONTAINER_RUNTIME="docker"
fi
# Load shared validation rules
source "$PROJECT_ROOT/config/validation.sh"
# Load shared helper functions (stack detection, site lifecycle)
source "$PROJECT_ROOT/scripts/helpers.sh"
source "$PROJECT_ROOT/scripts/presets-lib.sh"
source "$PROJECT_ROOT/scripts/ssl-lib.sh"
source "$PROJECT_ROOT/scripts/doctor-lib.sh"
source "$PROJECT_ROOT/scripts/export-import-lib.sh"
source "$PROJECT_ROOT/scripts/share-lib.sh"
source "$PROJECT_ROOT/scripts/ops-lib.sh"
source "$PROJECT_ROOT/scripts/site-types-lib.sh"
# Load compose manipulation helpers
source "$PROJECT_ROOT/scripts/compose-lib.sh"
# Main help
show_help() {
cat << EOF
🚀 SiteMatrix — PHP Stack Management
Usage: ./matrix <command> [options]
Site Management:
create <name> [--type=wp|laravel|symfony|drupal|joomla|grav] [--php-version=X.X] [--stack=nginx|apache]
Create new site (default type: wp, PHP: ${DEFAULT_PHP_VERSION}, stack: nginx)
PHP floors: wp 7.4, joomla/grav 8.1, laravel/symfony 8.2, drupal 8.3
--wp-version/--multisite/--preset and stack=apache are wp-only
edit <name> [--php-version=X.X] [--port=XXXX] Edit site configuration
rename <old> <new> [--delete-old] Rename an existing site
list List all sites
start <name> Start specific site
stop <name> Stop specific site
remove <name> Remove site
info <name> Show site details
rest <name> Show WordPress REST API routes for a site (JSON)
Environment Management:
install [name] One-command install (creates site and starts services)
start Start all sites, services, and frontend
stop Stop all services and frontend
restart Restart all services and frontend
status Show system status including frontend
logs [site] Show logs (all or specific site)
clean Clean up unused resources
Code Quality:
check [name] [path] Run code quality checks (all sites or specific site/path)
test Run tests
fix Auto-fix code issues
Site Maintenance:
clone <src> <dst> Clone existing site
reset <name> Reset site to fresh install
update <name> Update WordPress core
cache <name> Clear site caches
search-replace Database search/replace
backup <name> Backup site database/files (--schedule=<interval> --retention=N to schedule)
cron-install Install a site's scheduled backup into the crontab
cron-remove Remove a site's scheduled backup from the crontab
optimize [name] Apply performance optimizations to existing site(s)
Development:
shell <service> Access container shell (wp, db, nginx)
wp <site> <args> Run WP-CLI for a site
url <name> Show site URLs
import-db <n> <f> Import SQL dump (site name, file path)
export-db <n> [f] Export database (site name, optional file)
frontend <action> Manage frontend (start|stop|restart|status)
mail Open Mailpit email testing UI
phpmyadmin [site] Open phpMyAdmin (optionally preselect a site database)
xdebug <site> Enable Xdebug (always on) for site
scaffold <type> <name> Scaffold theme/plugin (theme|plugin)
preset <name> List available site presets
watch <site> Watch files and clear OPcache on changes
System:
repair [site] Repair missing configs, containers, and databases
health Run system health check
doctor Run environment/bootstrap checks (--json supported)
test Run compat matrix for a site (--wp-versions=6.4,6.5 --json)
export Export a site to a tar.gz bundle (manifest + files + db dump)
import Import a site from a tar.gz bundle
share Share a running site via cloudflared/ngrok/bore tunnel (--yes)
Frontend:
./matrix start # Starts frontend at http://$HOST_IP:8500
./matrix status # Shows frontend status
./matrix stop # Stops frontend with other services
./matrix token # Print dashboard LAN token (MATRIX_DASHBOARD_TOKEN)
./matrix token --rotate # Generate a new token (restart to apply)
EOF
}
# Get sites as array (for bash 3.2 compatibility)
get_sites_array() {
local temp_file=$(mktemp)
get_sites > "$temp_file"
echo "$temp_file"
}
# Create new site
create_site() {
local site_name=""
local php_version="$DEFAULT_PHP_VERSION"
local preset="blank"
local stack="nginx"
local type="wp"
local multisite=""
local wp_version=""
local skip_download=0
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-download)
skip_download=1
shift
;;
--php-version=*)
php_version="${1#*=}"
shift
;;
--php-version)
php_version="$2"
shift 2
;;
--stack=*)
stack="${1#*=}"
shift
;;
--stack)
stack="$2"
shift 2
;;
--type=*)
type="${1#*=}"
shift
;;
--type)
type="$2"
shift 2
;;
--preset=*)
preset="${1#*=}"
shift
;;
--preset)
preset="$2"
shift 2
;;
--multisite=*)
multisite="${1#*=}"
shift
;;
--wp-version=*)
wp_version="${1#*=}"
shift
;;
--wp-version)
wp_version="$2"
shift 2
;;
--multisite)
multisite="$2"
shift 2
;;
-*)
log_error "Unknown option: $1"
echo "Usage: ./matrix create <name> [--type=wp|laravel|symfony|drupal|joomla|grav] [--php-version=X.X] [--wp-version=X.X|latest] [--stack=nginx|apache] [--preset=preset-name] [--multisite=subdirectory|subdomain]"
return 1
;;
*)
if [[ -z "$site_name" ]]; then
site_name="$1"
fi
shift
;;
esac
done
if [[ -z "$site_name" ]]; then
read -p "Enter site name: " site_name
fi
if [[ -z "$site_name" ]]; then
log_error "Site name required"
return 1
fi
validate_site_name "$site_name" || return 1
validate_php_version "$php_version" || return 1
if ! validate_site_type "$type"; then
log_error "Invalid type: $type (use wp, laravel, symfony, drupal, joomla, grav)"
return 1
fi
if [[ "$stack" == "apache" && "$type" != "wp" ]]; then
log_error "Stack 'apache' is only supported for wp sites (type: $type)"
return 1
fi
local min_php
min_php=$(site_type_min_php "$type")
if ! php_at_least "$php_version" "$min_php"; then
log_error "Type '$type' requires PHP >= $min_php (got $php_version)"
return 1
fi
# WP-only flags rejected for non-wp types
if [[ "$type" != "wp" ]]; then
if [[ -n "$wp_version" ]]; then
log_error "--wp-version is only supported for wp sites (type: $type)"
return 1
fi
if [[ -n "$multisite" ]]; then
log_error "--multisite is only supported for wp sites (type: $type)"
return 1
fi
if [[ "$preset" != "blank" ]]; then
log_error "Presets are only supported for wp sites (type: $type)"
return 1
fi
fi
case "$stack" in
nginx|apache) ;;
*)
log_error "Invalid stack: $stack (use nginx or apache)"
return 1
;;
esac
# Validate multisite mode
if [[ -n "$multisite" ]]; then
case "$multisite" in
subdirectory|subdomain) ;;
*)
log_error "Invalid multisite mode: $multisite (use subdirectory or subdomain)"
return 1
;;
esac
fi
# Validate + resolve WP version pin
if [[ -n "$wp_version" ]]; then
validate_wp_version "$wp_version" || {
log_error "Invalid WP version: $wp_version (use X.Y like 6.4, or latest)"
return 1
}
wp_version=$(resolve_wp_version "$wp_version")
fi
if site_exists "$site_name"; then
log_error "Site '$site_name' already exists"
return 1
fi
local site_dir="$PROJECT_ROOT/wp_$site_name"
log_info "Creating $type site: $site_name (PHP $php_version, stack: $stack)"
# Create directory
mkdir -p "$site_dir"
mkdir -p "$PROJECT_ROOT/logs/wp_$site_name"
# Provision site files by type.
#
# Marker placement differs per type, and the reason is filesystem
# ownership, not taste:
# wp — markers are written HERE, before the container chowns the tree
# to www-data. After that the host user cannot write into the
# directory at all (that bug shipped once already).
# non-wp — composer create-project requires an EMPTY target, so markers
# must wait until provisioning finishes; by then the tree is
# www-data-owned, so provision_site_type writes them from inside
# a root container instead (write_markers_in_container).
if [[ "$type" == "wp" ]]; then
if [[ "$stack" == "apache" ]]; then
echo "apache" > "$site_dir/.matrix-stack"
else
echo "nginx" > "$site_dir/.matrix-stack"
fi
set_site_type "$site_name" "wp"
# Copy WordPress files using Docker/Podman
if [[ "$skip_download" -eq 0 ]]; then
log_info "Installing WordPress (PHP $php_version)..."
$CONTAINER_RUNTIME run --rm -v "$site_dir":/var/www/html wordpress:php${php_version}-fpm \
bash -c "cp -a /usr/src/wordpress/. /var/www/html/ && chown -R www-data:www-data /var/www/html"
# Install WP-CLI in site directory (persists via volume)
log_info "Installing WP-CLI..."
$CONTAINER_RUNTIME run --rm -v "$site_dir":/var/www/html wordpress:php${php_version}-fpm \
bash -c "curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x wp-cli.phar && mv wp-cli.phar /var/www/html/wp && chown www-data:www-data /var/www/html/wp"
# Pin WP core to --wp-version (Phase 7): overlay pinned core over image copy
if [[ -n "$wp_version" ]]; then
log_info "Pinning WordPress core to $wp_version..."
if wp_fetch_core "$wp_version" "$site_dir"; then
log_success "WordPress $wp_version pinned"
else
log_warning "WP core pin to $wp_version failed; keeping image default"
fi
fi
else
log_info "Skipping WP download (--skip-download: files restored from bundle)"
fi
elif [[ "$skip_download" -eq 1 ]]; then
# --skip-download means "restore files from a bundle" for every type, not
# just wp — otherwise import re-runs composer create-project and then
# overwrites every file it just wrote.
log_info "Skipping provisioning (--skip-download: files restored from bundle)"
else
# Non-wp types: shared matrix-php runtime image + per-type provisioning.
# provision_site_type also writes .matrix-stack/.matrix-type from inside
# a root container (see the marker-placement note above).
build_site_image "$php_version" || { rollback_site_create "$site_name"; return 1; }
provision_site_type "$site_name" "$type" "$php_version" "$stack" \
|| { rollback_site_create "$site_name"; return 1; }
fi
# wp with --skip-download: no container chowns the tree, so the markers
# written above are all that is needed.
# Copy baseline performance optimization files (wp only)
if [[ "$type" == "wp" && -d "$PROJECT_ROOT/config/optimizations" ]]; then
log_info "Applying performance optimizations..."
mkdir -p "$site_dir/wp-content/mu-plugins"
cp "$PROJECT_ROOT/config/optimizations/perf-mu-plugin.php" "$site_dir/wp-content/mu-plugins/"
cp "$PROJECT_ROOT/config/optimizations/object-cache.php" "$site_dir/wp-content/object-cache.php"
fi
# Apply preset if not blank (wp only — enforced above)
if [[ "$preset" != "blank" ]]; then
apply_preset "$site_name" "$preset" || return 1
fi
# Create site-specific nginx configuration (nginx stack only)
local nginx_conf_path="$PROJECT_ROOT/config/nginx/$site_name.conf"
if [[ "$stack" == "nginx" ]]; then
generate_nginx_conf "$site_name" "$type"
fi
if [[ ! -f "$COMPOSE_FILE" ]]; then
log_warning "docker-compose.yml missing - recreating base file before adding site"
rebuild_compose_base
fi
port=$(get_next_port)
compose_add_site "$site_name" "$php_version" "$port" "$nginx_conf_path" "$stack" "$type"
# Create database and grant permissions (all types except grav)
if site_type_uses_db "$type" && compose_or_container_running "wp_db"; then
$CONTAINER_RUNTIME exec -e MYSQL_PWD="${MYSQL_ROOT_PASSWORD:-root}" wp_db mysql -u root \
-e "CREATE DATABASE IF NOT EXISTS \`${site_name}_db\`; GRANT ALL PRIVILEGES ON \`${site_name}_db\`.* TO '${MYSQL_USER:-wp_user}'@'%'; FLUSH PRIVILEGES;" 2>/dev/null || true
fi
log_success "Site '$site_name' created successfully with PHP $php_version ($stack, type: $type)"
log_activity "CREATE" "$site_name" "Created with PHP $php_version, stack $stack, port $port, type $type"
echo "Direct: http://$HOST_IP:$port"
echo "Database: http://$HOST_IP:8200"
if [[ "$type" == "joomla" ]]; then
# Joomla's first-visit wizard asks for DB creds — print them for easy copy
echo "Joomla wizard DB: ${site_name}_db / user: ${MYSQL_USER:-wp_user} / password: ${MYSQL_PASSWORD:-wp_password}"
fi
# Auto-start the site after creation
log_info "Starting site '$site_name'..."
if [[ "$stack" == "apache" ]]; then
matrix_site_build_if_apache "$site_name" || return 1
fi
matrix_site_up "$site_name"
# Apply runtime performance tweaks (wp-config constants, phpredis, etc.)
[[ "$type" == "wp" ]] && apply_performance_tweaks "$site_name" "$php_version"
# Laravel: stock create-project runs migrations against the default .env
# (sqlite), which we then repoint at MySQL — the tables would be missing
# and first visit 500s. Migrate after start, when the DB exists.
# --skip-download (import) skips this: the bundle restore brings its own db.
if [[ "$type" == "laravel" && "$skip_download" -eq 0 ]] && compose_or_container_running "wp_db"; then
log_info "Running Laravel migrations..."
if $CONTAINER_RUNTIME exec -w /var/www/html "wp_$site_name" php artisan migrate --force >/dev/null 2>&1; then
log_success "Laravel migrations applied"
else
log_warning "Migrations not applied — run: $CONTAINER_RUNTIME exec wp_$site_name php artisan migrate"
fi
fi
log_success "Site '$site_name' started and ready!"
log_activity "START" "$site_name" "Auto-started after creation"
}
# Reload the PHP-FPM master process so a newly enabled extension (e.g. phpredis)
# is picked up by running workers. No-op if FPM isn't running. Finds the master
# via its args so it works whether FPM is PID 1 (official *-fpm images) or not
# (the apache stack, where apache is PID 1).
reload_fpm() {
local container="${1:-}"
[[ -z "$container" ]] && return 0
$CONTAINER_RUNTIME exec "$container" bash -c '
pid=$(ps -eo pid,args 2>/dev/null | grep "php-fpm: master" | grep -v grep | awk "{print \$1}" | head -1)
if [ -n "$pid" ]; then
kill -USR2 "$pid" 2>/dev/null && echo "reloaded FPM (pid $pid)"
fi
' 2>/dev/null || true
}
# Apply runtime performance tweaks to a running site's container.
# Installs phpredis (if missing) and appends performance constants to
# wp-config.php. Safe to re-run on existing sites.
apply_performance_tweaks() {
local site="${1:-}"
local php_version="${2:-$DEFAULT_PHP_VERSION}"
local container="wp_$site"
if ! $CONTAINER_RUNTIME ps --format '{{.Names}}' 2>/dev/null | grep -q "^$container$"; then
log_warning "Container $container not running — skipping runtime tweaks"
return 0
fi
# Install phpredis if missing (official Docker images don't bundle it)
local has_redis
has_redis=$($CONTAINER_RUNTIME exec "$container" php -m 2>/dev/null | grep -c "^redis$" || true)
if [[ "$has_redis" -eq 0 ]]; then
log_info "Installing phpredis in $container..."
# phpredis >=6 needs PHP 8.x; pin the last 5.x release for PHP 7.x.
local redis_pkg="redis"
[[ "$php_version" == 7.* ]] && redis_pkg="redis-5.3.7"
$CONTAINER_RUNTIME exec "$container" bash -c "
pecl install $redis_pkg 2>/dev/null && docker-php-ext-enable redis 2>/dev/null
" || log_warning "phpredis install failed (non-fatal)"
# Reload FPM so the newly enabled extension is loaded by running workers.
reload_fpm "$container"
fi
# Append performance constants to wp-config.php (idempotent)
log_info "Adding performance constants to wp-config.php..."
$CONTAINER_RUNTIME exec "$container" bash -c "
if ! grep -q 'Matrix performance' wp-config.php 2>/dev/null; then
cat >> wp-config.php << PHPEOF
// Matrix performance tweaks (applied by apply_performance_tweaks)
if ( ! defined( 'WP_CACHE' ) ) define( 'WP_CACHE', true );
if ( ! defined( 'WP_POST_REVISIONS' ) ) define( 'WP_POST_REVISIONS', 5 );
if ( ! defined( 'EMPTY_TRASH_DAYS' ) ) define( 'EMPTY_TRASH_DAYS', 7 );
if ( ! defined( 'DISABLE_WP_CRON' ) ) define( 'DISABLE_WP_CRON', true );
if ( ! defined( 'WP_MEMORY_LIMIT' ) ) define( 'WP_MEMORY_LIMIT', '256M' );
if ( ! defined( 'WP_MAX_MEMORY_LIMIT' ) ) define( 'WP_MAX_MEMORY_LIMIT', '512M' );
if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) define( 'WP_CACHE_KEY_SALT', '$site' );
if ( ! defined( 'WP_REDIS_HOST' ) ) define( 'WP_REDIS_HOST', 'redis' );
PHPEOF
fi
" 2>/dev/null || log_warning "Could not update wp-config.php (container may not be fully booted)"
log_success "Performance tweaks applied to $site"
}
# Apply performance optimizations to one or all existing sites.
# Usage: optimize_site [site-name]
optimize_site() {
local target="${1:-}"
if [[ -n "$target" ]]; then
require_site_type "$target" wp || return 1
# propagate optimize_single_site's status (it ran before this branch)
optimize_single_site "$target"
return $?
fi
log_info "Optimizing all existing sites..."
local temp_file
temp_file=$(get_sites_array)
local skipped=0
while IFS= read -r site; do
[[ -z "$site" ]] && continue
# get_site_type only — require_site_type would log a per-site error
# right before the summary line below.
if [[ "$(get_site_type "$site")" != "wp" ]]; then
skipped=$((skipped + 1))
continue
fi
optimize_single_site "$site"
done < "$temp_file"
rm -f "$temp_file"
[[ "$skipped" -gt 0 ]] && log_info "Skipped $skipped non-wp site(s) (optimize is WordPress-only)"
return 0
}
optimize_single_site() {
local site="$1"
local site_dir="$PROJECT_ROOT/wp_$site"
if [[ ! -d "$site_dir" ]]; then
log_error "Site '$site' not found at $site_dir"
return 1
fi
log_info "Optimizing site: $site"
# Copy optimization files
if [[ -d "$PROJECT_ROOT/config/optimizations" ]]; then
mkdir -p "$site_dir/wp-content/mu-plugins"
cp "$PROJECT_ROOT/config/optimizations/perf-mu-plugin.php" "$site_dir/wp-content/mu-plugins/"
cp "$PROJECT_ROOT/config/optimizations/object-cache.php" "$site_dir/wp-content/object-cache.php"
log_success "Optimization files copied to $site"
else
log_warning "config/optimizations directory not found — skipping file copy"
fi
# Apply runtime tweaks (container must be running)
local container="wp_$site"
if $CONTAINER_RUNTIME ps --format '{{.Names}}' 2>/dev/null | grep -q "^$container$"; then
# Detect PHP version from the container image
local php_version
php_version=$($CONTAINER_RUNTIME exec "$container" php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;' 2>/dev/null || echo "$DEFAULT_PHP_VERSION")
apply_performance_tweaks "$site" "$php_version"
else
log_warning "Container $container not running — start the site to apply runtime tweaks (phpredis + wp-config constants)"
echo " ./matrix start $site && ./matrix optimize $site"
fi
}
# Backup site
# Restore site
restore_site() {
local site_name="${1:-}"
local backup_file="${2:-}"
if [[ -z "$site_name" || -z "$backup_file" ]]; then
log_error "Usage: ./matrix restore <site-name> <backup-file>"
return 1
fi
validate_site_name "$site_name" || return 1
if [[ ! -f "$backup_file" ]]; then
log_error "Backup file not found: $backup_file"
return 1
fi
# Verify backup integrity before making any changes
if ! tar -tzf "$backup_file" >/dev/null 2>&1; then
log_error "Backup file is corrupt or invalid: $backup_file"
return 1
fi
log_info "Restoring site '$site_name' from backup..."
# Stop the site if running
matrix_site_stop "$site_name"
# Remove existing site files (ownership-safe for www-data-owned trees)
remove_site_dir "$PROJECT_ROOT/wp_$site_name" "$site_name" || return 1
# Extract backup
if ! tar -xzf "$backup_file" -C "$PROJECT_ROOT"; then
log_error "Failed to extract backup file"
return 1
fi
# Restore database if SQL dump exists in backup
local db_dump="$PROJECT_ROOT/wp_$site_name/database.sql"
if [[ -f "$db_dump" ]] && compose_or_container_running "wp_db"; then
log_info "Restoring database..."
$CONTAINER_RUNTIME exec -e MYSQL_PWD="${MYSQL_ROOT_PASSWORD:-root}" wp_db \
mysql -u root "${site_name}_db" < "$db_dump" 2>/dev/null
if [[ $? -eq 0 ]]; then
log_success "Database restored"
else
log_warning "Database restore failed — you may need to import manually"
fi
fi
# Start the site
matrix_site_up "$site_name"
log_success "Site '$site_name' restored"
}
list_sites_json() {
check_docker_json
local sites_json=""
local first_site=true
# Get sites via temp file (bash 3.2 compatible)
local sites_tmp
sites_tmp=$(mktemp)
get_sites > "$sites_tmp"
local site_list
site_list=$(cat "$sites_tmp")
rm -f "$sites_tmp"
while IFS= read -r site || [[ -n "$site" ]]; do
[[ -z "$site" ]] && continue
local status="stopped"
local port=""
if compose_or_container_running "wp_$site"; then
status="running"
fi
port=$(get_site_port "$site")
local php_version
php_version=$(get_site_php_version_from_compose "$site")
local stack
stack=$(get_site_stack "$site")
local site_type
site_type=$(get_site_type "$site")
local container_id=""
if [[ "$status" == "running" ]]; then
container_id=$($CONTAINER_RUNTIME ps -q -f "name=^/wp_${site}\$" 2>/dev/null | head -1)
[[ -z "$container_id" ]] && container_id=$($CONTAINER_RUNTIME ps -q -f name="wp_$site" 2>/dev/null | head -1)
fi
local local_url="null"
if [[ -n "$port" ]]; then
local_url="\"http://$HOST_IP:$port\""
fi
local disk_usage="null"
if [[ -d "$PROJECT_ROOT/wp_$site" ]]; then
disk_usage="\"$(du -sh "$PROJECT_ROOT/wp_$site" 2>/dev/null | cut -f1)\""
fi
local php_version_json="null"
if [[ -n "$php_version" ]]; then
php_version_json="\"$php_version\""
fi
local container_id_json="null"
if [[ -n "$container_id" ]]; then
container_id_json="\"$container_id\""
fi
local site_entry="{\"name\":\"$site\",\"status\":\"$status\",\"stack\":\"$stack\",\"type\":\"$site_type\",\"port\":${port:-null},\"phpVersion\":$php_version_json,\"containerId\":$container_id_json,\"localUrl\":$local_url,\"diskUsage\":$disk_usage}"
if [[ "$first_site" == true ]]; then
sites_json="$site_entry"
first_site=false
else
sites_json="$sites_json,$site_entry"
fi
done <<< "$site_list"
# Build services JSON
local services_json=""
local first_svc=true
local svc_names=("wp_db:Database:null" "wp_redis:Redis Cache:null" "wp_phpmyadmin:phpMyAdmin:8200" "wp_phpcs:PHP CodeSniffer:null" "wp_phpstan:PHPStan:null" "wp_cli:WP-CLI:null")
for svc in "${svc_names[@]}"; do
local svc_container="${svc%%:*}"
local remaining="${svc#*:}"
local svc_name="${remaining%%:*}"
local svc_port="${remaining##*:}"
local svc_status="stopped"
local svc_cid="null"
if $CONTAINER_RUNTIME ps -q -f name="$svc_container" 2>/dev/null | grep -q .; then
svc_status="running"
svc_cid="\"$($CONTAINER_RUNTIME ps -q -f name="$svc_container" 2>/dev/null | head -1)\""
fi
local svc_url="null"
if [[ "$svc_port" != "null" && -n "$svc_port" ]]; then
svc_url="\"http://$HOST_IP:$svc_port\""
fi
local svc_entry="{\"name\":\"$svc_name\",\"status\":\"$svc_status\",\"containerId\":$svc_cid,\"localUrl\":$svc_url}"
if [[ "$first_svc" == true ]]; then
services_json="$svc_entry"
first_svc=false
else
services_json="$services_json,$svc_entry"
fi
done
output_json "{\"success\":true,\"sites\":[$sites_json],\"services\":[$services_json]}"
}
# Get port from running container
# List all sites and services
list_sites() {
log_info "Sites & Services:"
echo ""
# Get WordPress sites - use temp file for bash 3.2 compatibility
local sites_tmp
sites_tmp=$(mktemp)
get_sites > "$sites_tmp"
local site_list
site_list=$(cat "$sites_tmp")
rm -f "$sites_tmp"
# Show WordPress sites
if [[ -n "$site_list" ]]; then
printf "%-15s %-10s %-8s %-30s %-30s\n" "WordPress Site" "Status" "Stack" "Local URL" "Domain URL"
echo "────────────────────────────────────────────────────────────────────────────────────"
while IFS= read -r site || [[ -n "$site" ]]; do
[[ -z "$site" ]] && continue
local status="Stopped"
local port=""
local stack
stack=$(get_site_stack "$site")
if compose_or_container_running "wp_$site"; then
status="Running"
fi
port=$(get_site_port "$site")
local port_str="Not configured"
if [[ -n "$port" ]]; then
port_str="http://$HOST_IP:$port"
fi
printf "%-15s %-10s %-8s %-30s %-30s\n" "$site" "$status" "$stack" \
"$port_str" \
"Direct port access only"
done <<< "$site_list"
echo ""
fi
# Show Management Services
printf "%-15s %-10s %-30s %-30s\n" "Service" "Status" "Local URL" "Description"
echo "────────────────────────────────────────────────────────────────────────"
# Database
local db_status="Stopped"
if compose_or_container_running "wp_db"; then
db_status="Running"
fi
printf "%-15s %-10s %-30s %-30s\n" "Database" "$db_status" \
"http://$HOST_IP:3306" \
"MySQL Database Server"
# Redis Cache
local redis_status="Stopped"
if compose_or_container_running "wp_redis"; then
redis_status="Running"
fi
printf "%-15s %-10s %-30s %-30s\n" "Redis Cache" "$redis_status" \
"Internal" \
"In-memory Cache"
# phpMyAdmin
local phpmyadmin_status="Stopped"
if compose_or_container_running "wp_phpmyadmin"; then
phpmyadmin_status="Running"
fi
printf "%-15s %-10s %-30s %-30s\n" "phpMyAdmin" "$phpmyadmin_status" \
"http://$HOST_IP:8200" \
"Database Management"
# Mailpit
local mailpit_status="Stopped"
if compose_or_container_running "wp_mailpit"; then
mailpit_status="Running"
fi
printf "%-15s %-10s %-30s %-30s\n" "Mailpit" "$mailpit_status" \
"http://$HOST_IP:8025" \
"Email Testing (SMTP: 1025)"
# Code Quality Tools
local phpcs_status="Stopped"
if compose_or_container_running "wp_phpcs"; then
phpcs_status="Running"
fi
printf "%-15s %-10s %-30s %-30s\n" "PHP CodeSniffer" "$phpcs_status" \
"Tools Profile" \
"Code Quality - WordPress Standards"
local phpstan_status="Stopped"
if compose_or_container_running "wp_phpstan"; then
phpstan_status="Running"
fi
printf "%-15s %-10s %-30s %-30s\n" "PHPStan" "$phpstan_status" \
"Tools Profile" \
"Code Quality - Static Analysis"
local wpcli_status="Stopped"
if compose_or_container_running "wp_cli"; then
wpcli_status="Running"
fi
printf "%-15s %-10s %-30s %-30s\n" "WP-CLI" "$wpcli_status" \
"Tools Profile" \
"WordPress Command Line"
echo ""
}
# Start a single site: compose up when the site is known to compose,
# otherwise start its existing containers directly (stale/missing compose).
cmd_start_site() {
local site_name="$1"
log_info "Starting site '$site_name'..."
if [[ "$(site_state "$site_name" | sed -n 's/^present=//p')" == "1" ]]; then
matrix_site_up "$site_name"
else
if $CONTAINER_RUNTIME ps -a --format "{{.Names}}" 2>/dev/null | grep -q "^wp_${site_name}$"; then
$CONTAINER_RUNTIME start "wp_${site_name}" 2>/dev/null || true
fi
if site_has_nginx "$site_name" && $CONTAINER_RUNTIME ps -a --format "{{.Names}}" 2>/dev/null | grep -q "^nginx_${site_name}$"; then
$CONTAINER_RUNTIME start "nginx_${site_name}" 2>/dev/null || true
fi
fi
log_success "Site '$site_name' started"
log_activity "START" "$site_name" "Started manually"
show_urls "$site_name"
}
# Start environment
start_env() {
log_info "Starting environment..."
$DOCKER_COMPOSE up -d db redis phpmyadmin 2>/dev/null || true
sleep 3
# Start frontend if available
if [[ -d "$PROJECT_ROOT/frontend" ]]; then
manage_frontend start
fi
log_success "Environment started"
echo ""
echo "Core services running: database, redis, phpMyAdmin"
echo ""
echo "Start sites individually: ./matrix start <site>"
# Show frontend URL
if [[ -f "$PROJECT_ROOT/.frontend.pid" ]]; then
local pid=$(cat "$PROJECT_ROOT/.frontend.pid")
if ps -p "$pid" > /dev/null 2>&1; then
echo "Frontend: http://$HOST_IP:8500"
fi
fi
}
# Stop environment
stop_env() {
log_info "Stopping environment..."
# Stop frontend first if running
if [[ -d "$PROJECT_ROOT/frontend" ]]; then
manage_frontend stop
fi
$DOCKER_COMPOSE down
log_success "Environment stopped"
}
show_status_json() {
check_docker_json
local containers_json=""
local first=true
local compose_output
compose_output=$($CONTAINER_RUNTIME ps --format "{{.Names}}\t{{.Status}}\t{{.Ports}}" 2>&1) || {
error_json "DOCKER_ERROR" "Failed to query Docker: $compose_output"
}
while IFS=$'\t' read -r name status ports; do
[[ -z "$name" ]] && continue
[[ "$name" =~ ^wordpress-matrix ]] && continue
local name_json="${name//\"/\\\"}"
local status_json="${status//\"/\\\"}"
local ports_json="${ports//\"/\\\"}"
local entry="{\"name\":\"$name_json\",\"status\":\"$status_json\",\"ports\":\"$ports_json\"}"
if [[ "$first" == true ]]; then
containers_json="$entry"
first=false
else
containers_json="$containers_json,$entry"
fi
done <<< "$compose_output"
# Frontend status