chore: initialize the project

This commit is contained in:
owaisahmed5300 2025-08-11 23:33:11 +05:00
commit 2c543b716c
31 changed files with 6135 additions and 0 deletions

34
.editorconfig Normal file
View file

@ -0,0 +1,34 @@
# top-most EditorConfig file
root = true

# Default for all files
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

# PHP files (PSR-12)
[*.php]
indent_style = space
indent_size = 4
max_line_length = 120
insert_final_newline = true

# YAML (e.g., PHPStan, ECS)
[*.{yml,yaml}]
indent_style = space
indent_size = 2

# JSON files
[*.json]
indent_style = space
indent_size = 2

# Markdown files
[*.md]
trim_trailing_whitespace = false

# Shell scripts
[*.sh]
end_of_line = lf

29
.gitattributes vendored Normal file
View file

@ -0,0 +1,29 @@
# Ensure consistent line endings (LF) across all text files
* text=auto eol=lf

# Force LF in specific file types (for stricter control)
*.md text eol=lf
*.json text eol=lf
*.xml text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.neon text eol=lf
*.php text eol=lf

# Exclude from GitHub language stats
/.github/** linguist-documentation

# Exclude from GitHub-generated ZIP archives
/tests export-ignore
/docker export-ignore
/bin export-ignore
/.git* export-ignore
/.husky export-ignore
/package.json export-ignore
/examples export-ignore
/package-lock.json export-ignore
/.release-it.json export-ignore
/.editorconfig export-ignore
/phpcs.xml.dist export-ignore
/phpstan.neon.dist export-ignore
/phpunit.xml.dist export-ignore

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,35 @@
---
name: Bug Report
about: Report a bug or issue
title: ''
labels: bug
assignees: ''
---

## Bug Description

A clear description of what the bug is.

## Steps to Reproduce

1.
2.
3.

## Expected Behavior

What you expected to happen.

## Actual Behavior

What actually happened.

## Environment

- PHP version:
- WordPress Version
- Library version:

## Additional Context

Add any other context about the problem here (error messages, logs, etc.).

View file

@ -0,0 +1,23 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: ''
labels: enhancement
assignees: ''
---

## Feature Description

A clear description of what you'd like to see added.

## Use Case

Explain why this feature would be useful and how you would use it.

## Proposed Solution

If you have ideas about how this could be implemented, describe them here.

## Additional Context

Add any other context and examples about the feature request here.

5
.github/SECURITY.md vendored Normal file
View file

@ -0,0 +1,5 @@
# Reporting a Vulnerability

If you discover a security-related issue, please report it privately by emailing **[security@wptechnix.com](mailto:security@wptechnix.com)**.

Avoid disclosing security vulnerabilities publicly.

3
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,3 @@
## Description

<!-- A brief summary of what this PR changes and the reason behind it ("Why" this change is necessary) -->

134
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,134 @@
name: Continuous Integration

on:
push:
branches: [ main, 'release/**' ]
paths-ignore:
# ignore markdowns and unrelated files
- '**.md'
- 'docker/**'
- '.husky/**'
- '.editorconfig'
- '.gitattributes'
- '.release-it.json'
- 'bin/copy'
- 'bin/docker'
- 'bin/composer'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/commitlint.yml'
- '.github/workflows/release.yml'

pull_request:
branches: [ main, 'release/**' ]
paths-ignore:
# ignore markdowns and unrelated files
- '**.md'
- 'docker/**'
- '.husky/**'
- '.editorconfig'
- '.gitattributes'
- '.release-it.json'
- 'bin/copy'
- 'bin/docker'
- 'bin/composer'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/commitlint.yml'
- '.github/workflows/release.yml'

jobs:
# PHPCS - Test with both minimum and maximum PHP versions
# This ensures coding standards work with different dependency versions
lint:
name: Code Style (PHP ${{ matrix.php-version }})
runs-on: ubuntu-latest
strategy:
matrix:
php-version: [8.0, 8.4]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
coverage: none

# Cache composer dependencies per PHP version
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-php${{ matrix.php-version }}-composer-

- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --no-progress

# For PRs: Only run on changed files (faster feedback)
- name: Run PHPCS on changed files (Pull Request)
if: github.event_name == 'pull_request'
run: |
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }} ${{ github.sha }} -- '*.php' || echo '')

if [[ -n "$CHANGED_FILES" ]]; then
echo "$CHANGED_FILES" | xargs ./vendor/bin/phpcs --report=checkstyle --no-cache > phpcs-report-${{ matrix.php-version }}.xml || true
else
echo "No PHP files changed. Skipping PHPCS."
echo '<checkstyle/>' > phpcs-report-${{ matrix.php-version }}.xml
fi

# Only annotate from one PHP version to avoid duplicate comments
- name: Create annotations from PHPCS report (Pull Request)
if: github.event_name == 'pull_request' && matrix.php-version == '8.0'
uses: staabm/annotate-pull-request-from-checkstyle-action@v1
with:
files: phpcs-report-${{ matrix.php-version }}.xml
notices-as-warnings: true

# For pushes to main: Full scan as final safety check
- name: Run PHPCS full scan (Push)
if: github.event_name != 'pull_request'
run: ./vendor/bin/phpcs --no-cache

# PHPStan - Run on highest PHP version for maximum coverage
static-analysis:
name: Static Analysis (PHPStan)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
tools: composer
coverage: none

- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-php8.4-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-php8.4-composer-

- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-progress

- name: Run PHPStan
run: ./vendor/bin/phpstan analyse --no-progress --error-format=github

33
.github/workflows/commitlint.yml vendored Normal file
View file

@ -0,0 +1,33 @@
# .github/workflows/lint.yml
name: Lint Commit Messages

on:
pull_request:
types: [opened, edited, synchronize]

jobs:
commitlint:
name: Lint Commits
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install npm dependencies
run: npm install

- name: Validate all commits in PR
uses: wagoid/commitlint-github-action@v6
with:
# When validating all commits in a PR, you need to use the
# base and head refs to determine the range of commits to lint.
# The action automatically gets these from the event payload.
# See: https://github.com/wagoid/commitlint-github-action#validating-all-commits-in-a-pr
first-parent: false

48
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,48 @@
# .github/workflows/release.yml
name: Release

on:
workflow_dispatch: # Allows manual triggering of the workflow
# push:
# branches:
# - main

jobs:
release:
name: Create Release
runs-on: ubuntu-latest
permissions:
contents: write # To push commits and tags
issues: write # To comment on issues and PRs
pull-requests: write # To create releases

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# We need to fetch all history and tags for release-it to work correctly
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Or your preferred Node.js version

- name: Install npm dependencies
run: npm install

- name: Check for uncommitted changes
run: |
echo "Running git status to check for modified files..."
git status

- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Create Release
run: npm run release -- --ci
env:
# The GITHUB_TOKEN is automatically provided by GitHub Actions
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

39
.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
# Composer
/vendor/
/composer.lock

# Node Modules
/node_modules/
# /package-lock.json

# PHPUnit
/phpunit.xml
/.phpunit.result.cache

# PHPCS
phpcs.xml

# PHPStan
phpstan.neon

# IDE
/.idea/
/.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs & Reports
*.log
*.tmp
/tmp/
/reports/
/logs/
/old/

# Environment
/docker/.env
/docker/php.ini

2
.husky/commit-msg Normal file
View file

@ -0,0 +1,2 @@
#!/bin/sh
npx --no-install commitlint --edit $1

49
.husky/pre-commit Normal file
View file

@ -0,0 +1,49 @@
#!/bin/bash

# Do not run on CI environments (e.g., GitHub Actions)
if [ "$CI" = "true" ]; then
echo "CI environment detected. Skipping pre-push checks."
exit 0
fi

# Get staged file list
# Use --diff-filter=d to exclude deleted files
changed_files=$(git diff --cached --name-only --diff-filter=d)

# Detect PHP or config changes
if ! echo "$changed_files" | grep -qE '\.php$|^phpstan\.neon(\.dist)?$|^phpcs\.xml(\.dist)?$'; then
echo "No PHP or lint config changes detected. Skipping pre-commit checks."
exit 0
fi

# Check if ./bin/composer exists and Docker is installed
if [ -f "./bin/composer" ] && command -v docker >/dev/null 2>&1; then
COMPOSER_CMD="./bin/composer"
elif command -v composer >/dev/null 2>&1; then
COMPOSER_CMD="composer"
else
echo "❌ Error: Neither ./bin/composer (with Docker installed) nor a global 'composer' command is available. Cannot run tests."
exit 1
fi

# Ensure vendor is available
if [ ! -d "vendor" ]; then
echo "Vendor directory not found. Running composer install..."
$COMPOSER_CMD install --no-interaction --prefer-dist --no-progress
fi

# Run checks
echo "Running phpcbf (auto-fixer)..."
$COMPOSER_CMD fix:phpcbf || true
echo "Adding fixes to git..."
echo "$changed_files" | grep -E '\.php$' | xargs git add

echo "Running phpcs..."
$COMPOSER_CMD lint:phpcs

echo "Running phpstan..."
$COMPOSER_CMD lint:phpstan

echo "✅ Pre-commit checks passed."

exit 0

24
.release-it.json Normal file
View file

@ -0,0 +1,24 @@
{
"$schema": "https://unpkg.com/release-it@19/schema/release-it.json",
"git": {
"commitMessage": "chore: release v${version}",
"tagName": "v${version}"
},
"github": {
"release": true,
"releaseName": "Release v${version}"
},
"npm": {
"publish": false
},
"plugins": {
"@release-it/bumper": {
"in": "composer.json",
"out": "composer.json"
},
"@release-it/conventional-changelog": {
"preset": "angular",
"infile": "CHANGELOG.md"
}
}
}

5
CHANGELOG.md Normal file
View file

@ -0,0 +1,5 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

18
LICENSE Normal file
View file

@ -0,0 +1,18 @@
The MIT License (MIT)

Copyright (c) 2025 WPTechnix

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# WPTechnix Settings Framework

A modern, object-oriented PHP framework for creating powerful and professional settings pages in WordPress. Designed with a clean, fluent API, this framework saves you time by abstracting away the complexities of the WordPress Settings API.

## License

Licensed under the MIT License.

64
bin/README.md Executable file
View file

@ -0,0 +1,64 @@
# Command-Line Scripts

This directory contains all the wrapper scripts used to manage and interact with the project's development environment. These scripts are designed to be run from the project root.

---

## `bin/docker`

This is the primary script for interacting with the Docker environment. It's a smart wrapper around `docker compose` that simplifies container management and command execution.

### Shorthand Container Access

The script's default behavior is to execute commands directly inside the main `app` container. Any command that is not a special management command (listed below) is passed through.

| Command | Description |
| ------------------------------------- | ------------------------------------------------------------------------- |
| `./bin/docker` | Open an interactive `bash` shell inside the default `app` container. |
| `./bin/docker <cmd...>` | Run any command with its arguments inside the `app` container. |
| **Example:** `./bin/docker php -v` | Checks the PHP version inside the container. |
| **Example:** `./bin/docker ls -la` | Lists files in the container's default working directory (`/app`). |

### Environment Management Commands

These special commands are used to control the Docker Compose stack.

| Command | Description |
| ------------------------------- | ------------------------------------------------------------------------- |
| `up` | Start all services defined in `docker-compose.yml` in detached mode. |
| `down` | Stop and remove all containers, networks, and volumes. |
| `build [service...]` | Rebuild and restart services (default: all). |
| `restart [service...]` | Restart one or more services (default: all). |
| `logs [service...]` | Follow log output from one or more services (default: all). |
| `exec <service> <cmd...>` | Execute a command in a **specific** service container. |
| **Example:** `exec db mysql` | Opens a MySQL command-line client inside the `db` container. |

---

## `bin/composer`

This is a dedicated wrapper script for Composer. It simplifies running Composer commands by automatically forwarding them to be executed inside the `app` container.

Instead of typing `./bin/docker composer <command>`, you can simply use:

| Command | Description |
| ----------------------------------------- | ------------------------------------------------- |
| `./bin/composer install` | Install all PHP dependencies from `composer.lock`.|
| `./bin/composer update` | Update PHP dependencies to their latest versions. |
| `./bin/composer require vendor/package` | Add a new PHP package to the project. |
| `./bin/composer remove vendor/package` | Remove a PHP package from the project. |

---

## Setup Scripts

These scripts are typically used only during the initial setup of the project.

### `bin/copy`

This script prepares your local environment by copying all necessary configuration files from their templates.

| Command | Description |
| ---------------------------- | ------------------------------------------------------------ |
| `./bin/copy` | Copies template files (e.g., `.dist`, `.example`) if the destination does not already exist. |
| `./bin/copy --override` | Forces the copy, overwriting any existing configuration files. |

22
bin/composer Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
#
# A wrapper script to run Composer commands inside the default app container.
# It forwards all arguments to the main ./bin/docker script.
#

# --- Strict Mode ---
# Ensures the script exits on error and handles variables safely.
set -euo pipefail

# --- Find the directory this script is in ---
# This is important to reliably locate the main ./bin/docker script,
# which should be in the same directory.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# The 'composer' command is passed as the first argument, followed by
# all the arguments this script received ($@).
#
# Example: `./bin/composer install --no-dev`
# Becomes: `./bin/docker composer install --no-dev`

exec "$SCRIPT_DIR/docker" composer "$@"

41
bin/copy Executable file
View file

@ -0,0 +1,41 @@
#!/bin/bash

set -e

OVERRIDE=false

# Parse options
while [[ "$#" -gt 0 ]]; do
case "$1" in
-o|--override)
OVERRIDE=true
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
shift
done

copy_file() {
local src="$1"
local dest="$2"

if [ -f "$src" ]; then
if [ "$OVERRIDE" = true ] || [ ! -f "$dest" ]; then
cp "$src" "$dest"
echo "Copied $src -> $dest"
else
echo "Skipped $dest (already exists, use -o to override)"
fi
fi
}

copy_file "docker/.env.example" "docker/.env"
copy_file "docker/php.ini.example" "docker/php.ini"
copy_file "phpstan.neon.dist" "phpstan.neon"
copy_file "phpcs.xml.dist" "phpcs.xml"
copy_file "phpunit.xml.dist" "phpunit.xml"

echo "Done."

126
bin/docker Executable file
View file

@ -0,0 +1,126 @@
#!/bin/bash
#
# A smart wrapper for Docker that simplifies container interactions.
# It is designed to be run from anywhere within the project.
#

# --- Strict Mode ---
# -e: exit on any error
# -u: exit on use of an unset variable
# -o pipefail: exit if any command in a pipeline fails
set -euo pipefail

# --- Configuration ---
# Path to the compose file, relative to the project root.
COMPOSE_FILE="docker/docker-compose.yml"
# Default service for shorthand commands.
DEFAULT_SERVICE="app"
# Default working directory inside the container.
DOCKER_WORKDIR="/app"


# --- Help Text ---
print_help() {
echo "Usage: ./bin/docker [command] [args...]"
echo ""
echo "A smart wrapper for Docker Compose that defaults to the '$DEFAULT_SERVICE' container."
echo "All arguments are passed to the executed command."
echo ""
echo "Shorthand Commands (for '$DEFAULT_SERVICE' container):"
echo " ./bin/docker # Open an interactive shell."
echo " ./bin/docker <cmd...> # Run a command (e.g., './bin/docker composer install --no-dev')."
echo ""
echo "Management Commands:"
echo " up [service...] Start services (default: all)."
echo " down Stop and remove all containers."
echo " build [service...] Rebuild and restart services."
echo " restart [service...] Restart services."
echo " logs [service...] Follow log output."
echo " exec <service> <cmd...> Explicitly run a command in a different container."
echo " help Show this help message."
echo ""
echo "Examples:"
echo " ./bin/docker composer update --dry-run"
echo " ./bin/docker phpcs --report=full src"
echo " ./bin/docker build"
echo " ./bin/docker exec db mysql -u root -p"
}

# --- Set Context to Project Root ---
# This ensures the script works regardless of where it's called from.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT"


# --- Core Function to Execute Commands in a Container ---
run_in_container() {
local service="$1"
shift # The rest of the arguments are the command to run.
local cmd_array=("$@")

# If no command was provided, default to an interactive bash shell.
if [ ${#cmd_array[@]} -eq 0 ]; then
cmd_array=("bash")
fi

echo "--- Ensuring '$service' is running..."
docker compose -f "$COMPOSE_FILE" up -d "$service" >/dev/null 2>&1

local container_id
container_id=$(docker compose -f "$COMPOSE_FILE" ps -q "$service")

if [ -z "$container_id" ]; then
echo "Error: Could not find or start a container for service '$service'." >&2
exit 1
fi

# Use interactive flags only if the script is running in a terminal.
local exec_flags=()
if [ -t 0 ]; then
exec_flags+=("-it")
fi

if [ "$DEFAULT_SERVICE" = "$service" ]; then
exec_flags+=("-w" "$DOCKER_WORKDIR")
fi

echo "--- Running in '$service': ${cmd_array[*]} ---"
exec docker exec "${exec_flags[@]}" "$container_id" "${cmd_array[@]}"
}


# --- Main Command Router ---
COMMAND="${1:-}" # Default to empty string if no args

case "$COMMAND" in
# Consolidate simple management commands
up|down|build|restart)
echo "--- Running management command: $COMMAND ${*:2} ---"
docker compose -f "$COMPOSE_FILE" "$COMMAND" "${@:2}"
;;

logs)
echo "--- Following logs for services: ${*:2} ---"
exec docker compose -f "$COMPOSE_FILE" logs -f "${@:2}"
;;

exec)
shift # Remove 'exec' from the arguments list
if [ -z "$1" ]; then
echo "Error: 'exec' command requires a service name." >&2
print_help
exit 1
fi
run_in_container "$@"
;;

help|--help)
print_help
;;

*)
# Default Case: Any other command is executed in the default container.
run_in_container "$DEFAULT_SERVICE" "$@"
;;
esac

48
composer.json Normal file
View file

@ -0,0 +1,48 @@
{
"name": "wptechnix/wp-settings-framework",
"version": "0.1.0",
"description": "A modern, fluent, and object-oriented framework for creating powerful WordPress admin settings pages.",
"type": "library",
"license": "MIT",
"keywords": [
"wordpress",
"settings",
"options"
],
"authors": [
{
"name": "WPTechnix",
"email": "developers@wptechnix.com"
}
],
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.0"
},
"require-dev": {
"phpcompatibility/php-compatibility": "*",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-strict-rules": "^2.0",
"squizlabs/php_codesniffer": "^3",
"szepeviktor/phpstan-wordpress": "^2.0"
},
"autoload": {
"psr-4": {
"WPTechnix\\WPSettings\\": "src/"
}
},
"scripts": {
"fix:phpcbf": "vendor/bin/phpcbf || true",
"lint:phpcs": "vendor/bin/phpcs --report=full",
"lint:phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --error-format=table",
"lint": [
"@fix:phpcbf",
"@lint:phpcs",
"@lint:phpstan"
]
},
"config": {
"sort-packages": true
}
}

14
docker/.env.example Normal file
View file

@ -0,0 +1,14 @@
PHP_VERSION=8.4
COMPOSE_PROJECT_NAME=pkg-wp-settings-framework

UID=1000
GID=1000

XDEBUG_MODE=off
XDEBUG_START_WITH_REQUEST=no
XDEBUG_DISCOVER_CLIENT_HOST=false
XDEBUG_CLIENT_HOST=host.docker.internal
XDEBUG_CLIENT_PORT=9000
XDEBUG_IDEKEY=PHPSTORM
XDEBUG_LOG_LEVEL=0
XDEBUG_MAX_NESTING_LEVEL=512

62
docker/Dockerfile Normal file
View file

@ -0,0 +1,62 @@
# Use build argument for dynamic PHP version
ARG PHP_VERSION=8.4
FROM php:${PHP_VERSION}-cli-alpine

# Set working directory
WORKDIR /app

RUN apk add --no-cache \
bash \
nano \
less \
curl \
linux-headers \
oniguruma-dev \
libxml2-dev \
icu-dev \
$PHPIZE_DEPS \
&& rm -rf /var/cache/apk/*

# Install PHP extensions commonly needed for development
RUN docker-php-ext-install \
bcmath \
exif \
intl \
mbstring

# Install and configure Xdebug
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug

# Install Composer (latest version)
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer


ARG USER_ID=1000
ARG GROUP_ID=1000

# Create a non-root user for security
RUN addgroup -g $GROUP_ID -S developer && \
adduser -u $USER_ID -S developer -G developer -h /home/developer

RUN chown -R $USER_ID:$GROUP_ID /home/developer /app

ENV COMPOSER_HOME=/home/developer/.composer
ENV COMPOSER_CACHE_DIR=/home/developer/.cache/composer
ENV PATH="/home/developer/.composer/vendor/bin:$PATH"
ENV WP_TESTS_BASE_DIR=/tmp/wordpress-tests-suite

RUN mkdir -p $COMPOSER_HOME $COMPOSER_CACHE_DIR && \
chown -R $USER_ID:$GROUP_ID $COMPOSER_HOME $COMPOSER_CACHE_DIR

RUN mkdir -p $WP_TESTS_BASE_DIR && \
chown -R $USER_ID:$GROUP_ID $WP_TESTS_BASE_DIR

RUN mkdir -p /app/logs /tmp/.phpunit.cache/code-coverage /tmp/.phpstan \
&& chown -R $USER_ID:$GROUP_ID /app/logs /tmp/.phpunit.cache/ /tmp/.phpunit.cache/code-coverage /tmp/.phpstan

# Switch to non-root user
USER developer

# Default command
CMD ["php", "-a"]

21
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,21 @@
services:
app:
build:
context: .
args:
USER_ID: ${UID:-1000}
GROUP_ID: ${GID:-1000}
PHP_VERSION: ${PHP_VERSION:-8.4}
container_name: ${COMPOSE_PROJECT_NAME:-wp-settings-framework}-php
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ../:/app
- ./php.ini:/usr/local/etc/php/conf.d/custom.ini
tty: true
stdin_open: true
networks:
- wp-settings-framework

networks:
wp-settings-framework:
driver: bridge

17
docker/php.ini.example Normal file
View file

@ -0,0 +1,17 @@
; Error reporting
error_reporting=E_ALL
display_errors=On
display_startup_errors=On
log_errors=On
error_log=/app/logs/php_errors.log

; Xdebug configuration
[xdebug]
xdebug.mode=${XDEBUG_MODE:-off}
xdebug.start_with_request=${XDEBUG_START_WITH_REQUEST:-no}
xdebug.discover_client_host=${XDEBUG_DISCOVER_CLIENT_HOST:-false}
xdebug.client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal}
xdebug.client_port=${XDEBUG_CLIENT_PORT:-9000}
xdebug.idekey=${XDEBUG_IDEKEY:-PHPSTORM}
xdebug.log_level=${XDEBUG_LOG_LEVEL:-0}
xdebug.max_nesting_level=${XDEBUG_MAX_NESTING_LEVEL:-512}

5023
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "wp-settings-framework",
"description": "A modern, fluent, and object-oriented framework for creating powerful WordPress admin settings pages.",
"license": "MIT",
"keywords": [
"wordpress",
"settings",
"options"
],
"version": "0.1.0",
"author": "WPTechnix <developers@wptechnix.com>",
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@release-it/bumper": "^7.0.5",
"@release-it/conventional-changelog": "^10.0.1",
"husky": "^9.1.7",
"release-it": "^19.0.4"
},
"directories": {
"test": "tests"
},
"scripts": {
"prepare": "husky",
"release": "release-it"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
}
}

32
phpcs.xml.dist Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0"?>
<ruleset name="Custom Coding standards">
<!-- Show sniff codes in output -->
<arg name="parallel" value="8"/>
<arg name="cache" value="/tmp/.phpcs.cache"/>
<arg name="report-width" value="120"/>
<arg name="extensions" value="php"/>

<config name="installed_paths" value="vendor/phpcompatibility/php-compatibility" />

<!-- PHP version -->
<config name="testVersion" value="8.0-"/>

<!-- Scan these directories -->
<file>src</file>

<!-- Exclude patterns -->
<exclude-pattern>*/bin/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>

<rule ref="PSR12" />
<rule ref="PHPCompatibility" />

<!-- Generic Rules -->
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="120"/>
<property name="absoluteLineLimit" value="160"/>
</properties>
</rule>
</ruleset>

21
phpstan.neon.dist Normal file
View file

@ -0,0 +1,21 @@
parameters:
phpVersion:
min: 80000
max: 80410

level: 8
tmpDir: /tmp/.phpstan
paths:
- src/

strictRules:
noVariableVariables: false

# ignoreErrors:
# - identifier: empty.notAllowed

treatPhpDocTypesAsCertain: false

includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/szepeviktor/phpstan-wordpress/extension.neon

View file

@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace WPTechnix\WPSettings\Interfaces;

/**
* Defines the contract for a settings field.
*/
interface FieldInterface
{
/**
* Render the field's HTML markup.
*
* This method is responsible for echoing the complete HTML for the form element.
*
* @param mixed $value The current value of the field.
* @param array<string, string|int|bool> $attributes Additional HTML attributes for the field.
*/
public function render(mixed $value, array $attributes): void;

/**
* Sanitize the field's value before saving.
*
* This method ensures the input data is clean and in the correct format
* before being persisted to the database.
*
* @param mixed $value The raw input value to be sanitized.
* @return mixed The sanitized value.
*/
public function sanitize(mixed $value): mixed;

/**
* Get the default value for the field.
*
* Provides a fallback value when no value has been saved yet.
*
* @return mixed The default value.
*/
public function getDefaultValue(): mixed;
}

View file

@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace WPTechnix\WPSettings\Interfaces;

/**
* Defines the contract for a settings page builder.
*/
interface SettingsInterface
{
/**
* Sets or overrides the main page title.
*
* @param string $pageTitle The new title for the settings page.
* @return static
*/
public function setPageTitle(string $pageTitle): static;

/**
* Sets or overrides the menu title.
*
* @param string $menuTitle The new title for the admin menu item.
* @return static
*/
public function setMenuTitle(string $menuTitle): static;

/**
* Adds a tab for organizing settings sections.
*
* @param string $id The unique identifier for the tab.
* @param string $title The text to display on the tab.
* @param string $icon Optional. A Dashicon class to display next to the title.
* @return static
*/
public function addTab(string $id, string $title, string $icon = ''): static;

/**
* Adds a settings section to the page.
*
* @param string $id The unique identifier for the section.
* @param string $title The title displayed for the section.
* @param string $description Optional. A description displayed below the section title.
* @param string $tabId Optional. The ID of the tab this section should appear under.
* @return static
*/
public function addSection(string $id, string $title, string $description = '', string $tabId = ''): static;

/**
* Adds a field to a section.
*
* @param string $id The unique identifier for the field.
* @param string $sectionId The ID of the section this field belongs to.
* @param string $type The field type (e.g., 'text', 'toggle', 'code').
* @param string $label The label displayed for the field.
* @param array<string, mixed> $args Optional. An array of additional arguments.
* @return static
*/
public function addField(string $id, string $sectionId, string $type, string $label, array $args = []): static;

/**
* Initializes the settings page and hooks all components into WordPress.
*
* This method must be called after all configuration is complete.
*/
public function init(): void;

/**
* Gets the WordPress option name where settings are stored.
*
* @return string The option name.
*/
public function getOptionName(): string;

/**
* Retrieves a setting's value for this settings page.
*
* @param string $key The unique key of the setting to retrieve.
* @param mixed $default A fallback value to return if the setting is not found.
* @return mixed The stored setting value, or the default if not found.
*/
public function get(string $key, mixed $default = null): mixed;
}