openspring / springphp-framework
The SpringPHP framework
Package info
gitlab.com/openspring-projects/springphp-framework
pkg:composer/openspring/springphp-framework
Requires
- php: ~7.4
Requires (Dev)
- phpunit/phpunit: ^8
README
A PHP 7.4+ framework inspired by Java Spring, bringing annotation-driven dependency injection, RESTful controllers, ORM, JWT security, and validation to PHP.
Table of Contents
Features
- Annotation-driven architecture — stereotypes (
@Component,@Service,@Configuration), DI (@Autowired,@Value), and REST mappings parsed from PHP docblocks - IoC Container — singleton and prototype scoped beans with lazy loading and cyclic dependency detection
- RESTful routing —
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PatchMappingwith path variables, content negotiation, and automatic parameter binding - ORM — typed column mapping, fluent query builder, parameter binding, and state machine entity lifecycle
- JWT Security — token processing with role-based and group-based access control
- Validation — constraint-based validation (
@NotNull,@NotBlank,@Email,@Min,@Max,@Size,@Regex,@Positive,@Past,@Future, etc.) - HTTP layer — request/response abstraction,
ResponseEntity, message converters (JSON, HTML), cookies, headers, CORS - Interceptors —
preHandle/postHandlehooks for cross-cutting concerns - Logging — built-in logger with Log4p implementation
- Testing —
TestRunnerfor bootstrapping in TEST profile, Mockito-style mocking withwhen/thenReturn - i18n —
ResourceBundleMessageSourcefor multi-language message loading
Requirements
- PHP 7.4 or higher
- Composer
- MySQL (primary supported database)
Installation
For existing projects
composer require openspring/springphp-framework
For new projects
We recommend starting from the springphp-sample boilerplate, which provides a ready-to-use project structure with default configurations.
You can also use the springphp-initializer CLI to scaffold new projects, generate the database access layer, request mappings, API documentation and build deliveries.
Quick Start
1. Create the application entry point
<?php
use Openspring\SpringphpFramework\Core\SpringphpApplication;
/**
* @ComponentScan(baseDirs = ["src/controllers", "src/services"])
* @PropertySource("config/application.properties")
*/
class Application extends SpringphpApplication
{
public function __construct()
{
parent::__construct(__DIR__, 'config/', 'config/test/');
}
}
$app = new Application();
$app->execute();
2. Create a REST controller
<?php
use Openspring\SpringphpFramework\Web\RestController;
use Openspring\SpringphpFramework\Http\ResponseEntity;
use Openspring\SpringphpFramework\Http\HttpStatus;
class UserController extends RestController
{
/**
* @Autowired(true)
* @var UserService
*/
private $userService;
/**
* @GetMapping(value = "/users", consumes = "application/json", produces = "application/json")
*/
public function getUsers()
{
$users = $this->userService->findAll();
return ResponseEntity::ok($users);
}
/**
* @GetMapping(value = "/users/{$id}", consumes = "application/json", produces = "application/json")
*/
public function getUser($id)
{
$user = $this->userService->findById($id);
return ResponseEntity::ok($user);
}
/**
* @PostMapping(value = "/users", consumes = "application/json", produces = "application/json")
*/
public function createUser(RequestBody $body)
{
$user = $this->userService->create($body);
return ResponseEntity::status(HttpStatus::CREATED, $user);
}
}
3. Create a service
<?php
use Openspring\SpringphpFramework\Core\Stereotype\Service;
/**
* @Service
*/
class UserService extends Service
{
/**
* @Autowired(true)
* @var UserDao
*/
private $userDao;
public function findAll(): array
{
return $this->userDao->findAll()->getResultList();
}
public function findById(int $id)
{
return $this->userDao->findById($id);
}
}
4. Create an entity
<?php
use Openspring\SpringphpFramework\Orm\DaoSupport;
use Openspring\SpringphpFramework\Orm\Column;
use Openspring\SpringphpFramework\Orm\Type\DataType;
use Openspring\SpringphpFramework\Orm\Type\PrimaryKey;
class UserDao extends DaoSupport
{
public $id;
public $name;
public $email;
public $password;
public $createdAt;
public function __construct()
{
parent::__construct('#__users');
$this->id = new Column($this, 'id', 11, DataType::INT, new PrimaryKey());
$this->name = new Column($this, 'name', 100, DataType::VARCHAR);
$this->email = new Column($this, 'email', 150, DataType::VARCHAR, null, true);
$this->password = new Column($this, 'password', 255, DataType::HASH);
$this->createdAt = new Column($this, 'created_at', 0, DataType::DATETIME);
$this->initialize();
}
}
Documentation
Application Bootstrap
The framework follows a structured boot sequence:
SpringphpApplicationconstructor loadsapplication.propertiesand sets up theEnvironmentApplicationContext::initialize()reads the.contextcache (bean registry + route mappings)DatabaseContext::initialize()configures data sources- On HTTP request:
Router::route()matches the URL, thenRestController::runAction()dispatches the action
REST Controllers
Controllers extend RestController and use docblock annotations for routing:
/**
* @GetMapping(value = "/products/{$id}", produces = "application/json")
*/
public function getProduct($id) { ... }
/**
* @PostMapping(value = "/products", consumes = "application/json", produces = "application/json")
*/
public function createProduct(RequestBody $body) { ... }
/**
* @PutMapping(value = "/products/{$id}", consumes = "application/json", produces = "application/json")
*/
public function updateProduct($id, RequestBody $body) { ... }
/**
* @DeleteMapping(value = "/products/{$id}", produces = "application/json")
*/
public function deleteProduct($id) { ... }
Available mapping annotations: @RequestMapping, @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping
Parameter binding: RequestBody, RequestHeader, RequestCookie, RequestTemporaryFile
Dependency Injection
The IoC container manages beans as lazy-loaded singletons (default) or prototypes.
/**
* @Service
*/
class OrderService extends Service
{
/**
* @Autowired(true)
* @var OrderDao
*/
private $orderDao;
/**
* @Autowired(true)
* @var NotificationService
*/
private $notificationService;
/** @Value("app.order.max-items") */
private $maxItems;
}
@Autowired(true)— injects a bean by type (field injection); requires@varon a separate line@Value("property.key")— injects a value fromapplication.properties- Stereotypes:
@Component,@Service,@Configuration - Cyclic dependency detection is built in
ORM & Entities
Entities extend DaoSupport and define typed columns:
class ProductDao extends DaoSupport
{
public $id;
public $name;
public $price;
public $active;
public function __construct()
{
parent::__construct('#__products');
$this->id = new Column($this, 'id', 11, DataType::INT, new PrimaryKey());
$this->name = new Column($this, 'name', 200, DataType::VARCHAR, null, false, true);
$this->price = new Column($this, 'price', 10, DataType::FLOAT);
$this->active = new Column($this, 'active', 1, DataType::BOOLEAN);
$this->initialize();
}
}
Supported column types: INT, VARCHAR, FLOAT, BOOLEAN, DATE, DATETIME, JSON, GUID, HASH
The #__ prefix in table names is automatically replaced by the configured table prefix from your data source.
Query Builder
Fluent API for building SQL queries with parameter binding:
$results = $productDao->select(['id', 'name', 'price'])
->from()
->where('active', '=', 1)
->and('price', '>', ':minPrice')
->setParameter(':minPrice', 25.00)
->orderBy('name', 'ASC')
->getResultList();
Validation
Constraint-based validation using docblock annotations on method parameters:
| Constraint | Description |
|---|---|
@NotNull | Value must not be null |
@NotBlank | String must not be null or empty |
@NotEmpty | Collection/string must not be empty |
@Email | Must be a valid email address |
@Min(value) | Numeric minimum |
@Max(value) | Numeric maximum |
@Size(min, max) | String/array length bounds |
@Regex(pattern) | Must match the regular expression |
@Positive / @PositiveOrZero | Must be positive (or zero) |
@Negative / @NegativeOrZero | Must be negative (or zero) |
@Past / @PastOrPresent | Date must be in the past |
@Future / @FutureOrPresent | Date must be in the future |
@AssertTrue / @AssertFalse | Boolean assertions |
@URL | Must be a valid URL |
@InArray(values) | Must be one of the specified values |
Security (JWT)
JWT-based authentication with role and group access control:
/**
* @GetMapping(value = "/admin/dashboard", produces = "application/json", groupIn = "1,2", roleIn = "ADMIN")
*/
public function dashboard() { ... }
groupIn— comma-separated list of authorized group IDs ("*"for all authenticated users)roleIn— comma-separated list of authorized roles ("*"for all authenticated users)
Implement IJwtProcessor to handle token extraction and validation, and IUser for the current user model.
Configuration
Configuration is managed through properties files:
# application.properties
#################### COMMON CONFIG #########################
# application display name
springphp.application.name=appName
# project mode; supported (values: DEV|QUALIF|VAL|PPROD|REHEARSAL|PROD)
springphp.profile.active=DEV
# relative web home url; put / if the application is on the www root or something like /app1/ in other case
springphp.application.context.path=/relative/path/to/api/
# base is {rootPath}: application absolute public path
springphp.path.public=/relative/path/to/public/directory/
# base is {rootPath}: main sources relative path
springphp.path.sources=src/main/php/
# base is {rootPath}: test sources relative path
springphp.path.tests=src/test/php/
#################### DATABASE CONFIG ####################
# databases config names to connect to on application startup (config names must be separated by commas)
# Exemples: defaultDatabase, customersDatabase, clientsDatabase, commandesDatabase
springphp.database.targets=defaultDatabase
####################### LOG CONFIG #########################
# enable log. If false all logging is disabled !
springphp.log.enable=false
# error logging file path
springphp.log.file=logs\server_%d.log
# log file max size in KO (example: 1024 ; 2048)
springphp.log.file.maxsize=1024
# log level [SQL | DEBUG | INFO | WARN | ERROR]
springphp.log.level=SQL
# log output format: known var names: %d (date) ; %l (log level) ; %m (message)
springphp.log.format=[%d][%ns][%c][%l] %m
# application.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<orm-configuration>
<datasources>
<!-- default datasource -->
<datasource id="defaultDatabase">
<!-- connection parametrs -->
<dialect>Openspring\SpringphpFramework\Orm\Driver\Mysql\MysqlDriver</dialect>
<host>localhost</host>
<database>db_name</database>
<port>3306</port>
<user>root</user>
<password></password>
<tableprefix>tbl_</tableprefix>
</datasource>
</datasources>
<!-- show sql when executing queries -->
<show_sql>true</show_sql>
</orm-configuration>
Profile-based configuration: Use dev-application.properties, test-application.properties, or prod-application.properties for environment-specific overrides.
Access properties in code:
// Via annotation
/** @Value("app.name") */
private $appName;
// Programmatically
$value = Environment::getProperty("app.name");
Interceptors
Implement HttpInterceptor for cross-cutting concerns like logging, auth checks, or metrics:
/**
* @Component
*/
class AuthInterceptor extends HttpInterceptor
{
public function preHandle(): bool
{
// Return false to halt the request
return true;
}
public function postHandle(): bool
{
return true;
}
}
Logging
Built-in logging with LogFactory:
$logger = LogFactory::getLogger(self::class);
$logger->info('Processing order');
$logger->debug('Order details: ' . $orderId);
$logger->error('Payment failed');
Testing
# Run all tests
vendor/bin/phpunit --configuration phpunit.xml
# Run a specific test class
vendor/bin/phpunit --filter UserServiceTest
# Run a specific test method
vendor/bin/phpunit --filter testCreateUser
# Run with coverage
vendor/bin/phpunit --coverage-text --configuration phpunit.xml
The framework provides a TestRunner that bootstraps the application in TEST profile and allows simulating HTTP requests. A Mockito-style mocking utility is also available:
use Openspring\SpringphpFramework\Mock\Mockito;
$mock = Mockito::mock(UserService::class);
Mockito::when($mock, 'findById')->thenReturn($expectedUser);
Project Structure
springphp-framework/
├── src/
│ ├── main/php/Openspring/SpringphpFramework/
│ │ ├── Annotation/ # Docblock annotation parsing
│ │ ├── Context/ # ApplicationContext, DatabaseContext, Environment
│ │ ├── Core/ # Bean, Component, Service, SpringphpApplication
│ │ ├── Enumeration/ # Framework enums
│ │ ├── Exception/ # Exception hierarchy (HTTP, Bean, Data, IO)
│ │ ├── Http/ # Request, Response, ResponseEntity, converters
│ │ ├── IO/ # File and directory utilities
│ │ ├── Log/ # Logger and Log4p implementation
│ │ ├── Mail/ # Mail sender
│ │ ├── Mock/ # Mockito-style test mocking
│ │ ├── Orm/ # DaoSupport, Column, QueryBuilder, drivers
│ │ ├── Reflection/ # ClassReflection for docblock parsing
│ │ ├── Security/ # JWT processing, access control
│ │ ├── Type/ # ArrayList, Any, DynamicObject
│ │ ├── Utils/ # Strings, Arrays, Dates, Encryption, etc.
│ │ ├── Validation/ # Constraint-based validation
│ │ └── Web/ # RestController, Router, RestTemplate
│ └── test/php/TestCase/ # Unit tests (mirrors main structure)
├── composer.json
├── phpunit.xml
└── LICENSE.txt
The project follows a Maven-style directory layout with PSR-4 autoloading.
Contributing
There are currently a number of outstanding issues that need to be addressed. Please read How to contribute.
You can report bugs and request features on the issue tracker.
License
This project is licensed under the MIT License. See LICENSE.txt for details.