使用yii3实现一个微框架
yii3 是一个现代的php框架,全面符合PSR标准,由一百多个独立包组成,实现按需加载
这篇文章是描述如何使用很少的 yii3 组件实现一个微框架
新建项目目录
前置的依赖
- php 8.2
- composer 2
- git
安装命令
mkdir yii3-mrico;
cd yii3-mrico;
composer init;
composer require \
httpsoft/http-server-request \
yiisoft/yii-http \
yiisoft/router-fastroute \
yiisoft/di \
yiisoft/injector \
yiisoft/event-dispatcher \
yiisoft/yii-event \
yiisoft/psr-emitter ;
编写入口文件
index.php
<?php
/**
* Yii3 Micro Framework - Single File Application
*/
// =============================================================================
// 0. Autoloading
// =============================================================================
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use HttpSoft\Message\Response;
use HttpSoft\Message\ResponseFactory;
use HttpSoft\Message\ServerRequest;
use HttpSoft\Message\ServerRequestFactory;
use HttpSoft\Message\StreamFactory;
use HttpSoft\Message\UriFactory;
use HttpSoft\Message\UploadedFileFactory;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;
use Yiisoft\EventDispatcher\Dispatcher\Dispatcher;
use Yiisoft\EventDispatcher\Provider\Provider;
use Yiisoft\Http\Status;
use Yiisoft\Injector\Injector;
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;
use Yiisoft\Middleware\Dispatcher\MiddlewareFactory;
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\Group;
use Yiisoft\Router\Middleware\Router;
use Yiisoft\Router\Route;
use Yiisoft\Router\RouteCollection;
use Yiisoft\Router\RouteCollectionInterface;
use Yiisoft\Router\RouteCollector;
use Yiisoft\Router\RouteCollectorInterface;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Router\UrlMatcherInterface;
use Yiisoft\Router\FastRoute\UrlGenerator;
use Yiisoft\Router\FastRoute\UrlMatcher;
use Yiisoft\Yii\Event\CallableFactory;
use Yiisoft\Yii\Event\ListenerCollectionFactory;
use Yiisoft\Yii\Http\Application;
use Yiisoft\Yii\Http\Event\AfterEmit;
use Yiisoft\Yii\Http\Event\AfterRequest;
use Yiisoft\Yii\Http\Event\ApplicationShutdown;
use Yiisoft\Yii\Http\Event\ApplicationStartup;
use Yiisoft\Yii\Http\Event\BeforeRequest;
use Yiisoft\Yii\Http\Handler\NotFoundHandler;
// =============================================================================
// 1. Event Listener Configuration
// =============================================================================
function createListenerProvider(ContainerInterface $container): ListenerProviderInterface
{
$listenerConfig = [
ApplicationStartup::class => [
static function (ApplicationStartup $event) {
error_log('[EVENT] Application starting up');
},
],
ApplicationShutdown::class => [
static function (ApplicationShutdown $event) {
error_log('[EVENT] Application shutting down');
},
],
BeforeRequest::class => [
static function (BeforeRequest $event) {
$request = $event->getRequest();
error_log('[EVENT] Before request: ' . $request->getMethod() . ' ' . $request->getUri()->getPath());
},
],
AfterRequest::class => [
static function (AfterRequest $event) {
$response = $event->getResponse();
$status = $response ? $response->getStatusCode() : 'no response';
error_log('[EVENT] After request, status: ' . $status);
},
],
AfterEmit::class => [
static function (AfterEmit $event) {
error_log('[EVENT] Response emitted');
},
],
];
$injector = new Injector($container);
$callableFactory = new CallableFactory($container, $injector);
$factory = new ListenerCollectionFactory($injector, $callableFactory);
$collection = $factory->create($listenerConfig);
return new Provider($collection);
}
// =============================================================================
// 2. Route Configuration
// =============================================================================
function configureRoutes(RouteCollectorInterface $collector): void
{
$routes = [
// Home page
'home' => Route::get('/')
->action(function (ResponseFactoryInterface $responseFactory) {
$response = $responseFactory->createResponse(Status::OK);
$response->getBody()->write(json_encode([
'framework' => 'Yii3 Micro',
'status' => 'running',
'time' => date('Y-m-d H:i:s'),
'php' => PHP_VERSION,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $response
->withHeader('Content-Type', 'application/json');
}),
// Health check
'health' => Route::get('/health')
->action(function (ResponseFactoryInterface $responseFactory) {
$response = $responseFactory->createResponse(Status::OK);
$response->getBody()->write(json_encode([
'status' => 'healthy',
'php' => PHP_VERSION,
]));
return $response
->withHeader('Content-Type', 'application/json');
}),
// Hello with name parameter
'hello' => Route::get('/hello/{name}')
->action(function (
ResponseFactoryInterface $responseFactory,
CurrentRoute $currentRoute,
) {
$name = $currentRoute->getArgument('name', 'World');
$response = $responseFactory->createResponse(Status::OK);
$response->getBody()->write(json_encode([
'message' => "Hello, {$name}!",
'route' => $currentRoute->getUri()->getPath(),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $response
->withHeader('Content-Type', 'application/json');
}),
// Echo POST request body
'echo' => Route::post('/echo')
->action(function (
ServerRequestInterface $request,
ResponseFactoryInterface $responseFactory,
) {
$body = $request->getParsedBody() ?? [];
$response = $responseFactory->createResponse(Status::OK);
$response->getBody()->write(json_encode([
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
'body' => $body,
'query' => $request->getQueryParams(),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $response
->withHeader('Content-Type', 'application/json');
}),
// Get URL by name (demonstrates URL generation)
'urls' => Route::get('/urls')
->action(function (
ResponseFactoryInterface $responseFactory,
UrlGeneratorInterface $urlGenerator,
\Yiisoft\Router\RouteCollectorInterface $collector,
) {
$getRouteName = function($route) {
return $route->getData('name');
};
$getRouteGroupName = function($routeGroup) use (&$getRouteGroupName, $getRouteName) {
$list = [];
foreach ($routeGroup->getData('routes') as $item) {
if ($item instanceof Group) {
$list = array_merge($list, $getRouteGroupName($item));
} else {
$list[] = $getRouteName($item);
}
}
return $list;
};
$routeList = [];
foreach ($collector->getItems() as $item) {
if ($item instanceof Group) {
$routeList = array_merge($routeList, $getRouteGroupName($item));
} else {
$routeList[] = $getRouteName($item);
}
}
$response = $responseFactory->createResponse(Status::OK);
$response->getBody()->write(json_encode([
'routes' => [
'home' => $urlGenerator->generate('home'),
'health' => $urlGenerator->generate('health'),
'hello' => $urlGenerator->generate('hello', ['name' => 'Yii3']),
'echo' => $urlGenerator->generate('echo'),
'urls' => $urlGenerator->generate('urls'),
'routeList' => $routeList,
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $response
->withHeader('Content-Type', 'application/json');
}),
// Demo of dependency injection
'di-test' => Route::get('/di-test')
->action(function (
ResponseFactoryInterface $responseFactory,
ContainerInterface $container,
EventDispatcherInterface $eventDispatcher,
UrlMatcherInterface $urlMatcher,
UrlGeneratorInterface $urlGenerator,
\Yiisoft\Router\RouteCollectorInterface $collector,
) {
$response = $responseFactory->createResponse(Status::OK);
$response->getBody()->write(json_encode([
'di_working' => true,
'container_class' => get_class($container),
'event_dispatcher_class' => get_class($eventDispatcher),
'url_matcher_class' => get_class($urlMatcher),
'url_generator_class' => get_class($urlGenerator),
'url_collector_class' => get_class($collector),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $response
->withHeader('Content-Type', 'application/json');
}),
];
$collector->addRoute(
Group::create('/' . basename(__FILE__))->routes(
...array_map(function($key, $item){return $item->name(basename(__FILE__) . '/' . $key);}, array_keys($routes), array_values($routes))
),
...array_map(function($key, $item){return $item->name($key);}, array_keys($routes), array_values($routes)),
);
$collector->addRoute(
Route::methods(
[\Yiisoft\Http\Method::GET, \Yiisoft\Http\Method::POST, \Yiisoft\Http\Method::PUT, \Yiisoft\Http\Method::DELETE, \Yiisoft\Http\Method::PATCH, \Yiisoft\Http\Method::OPTIONS],
'/{path:.*}'
)
->action(static function (ResponseFactoryInterface $responseFactory): ResponseInterface {
$response = $responseFactory->createResponse(Status::NOT_FOUND);
$response->getBody()->write('404 NOT FOUND');
return $response;
})
->name('fallback.404')
);
}
// =============================================================================
// 3. DI Container Configuration
// =============================================================================
// Build routes first (needed for RouteCollection)
$routeCollector = new RouteCollector();
configureRoutes($routeCollector);
$routeCollection = new RouteCollection($routeCollector);
$containerConfig = ContainerConfig::create()
->withDefinitions([
// --- PSR-17 HTTP Factories ---
ResponseFactoryInterface::class => ResponseFactory::class,
ServerRequestFactoryInterface::class => ServerRequestFactory::class,
StreamFactoryInterface::class => StreamFactory::class,
UriFactoryInterface::class => UriFactory::class,
UploadedFileFactoryInterface::class => UploadedFileFactory::class,
// --- Router (use pre-built instances) ---
RouteCollectionInterface::class => static fn () => $routeCollection,
RouteCollectorInterface::class => static fn () => $routeCollector,
CurrentRoute::class => CurrentRoute::class,
UrlMatcherInterface::class => UrlMatcher::class,
UrlGeneratorInterface::class => UrlGenerator::class,
// --- Event Dispatcher ---
ListenerProviderInterface::class => static fn (ContainerInterface $c) => createListenerProvider($c),
EventDispatcherInterface::class => Dispatcher::class,
// --- Middleware ---
MiddlewareFactory::class => static fn (ContainerInterface $c) => new MiddlewareFactory($c),
Router::class => static function (ContainerInterface $c) {
return new Router(
$c->get(UrlMatcherInterface::class),
$c->get(ResponseFactoryInterface::class),
$c->get(MiddlewareFactory::class),
$c->get(CurrentRoute::class),
$c->get(EventDispatcherInterface::class),
);
},
]);
$container = new Container($containerConfig);
// =============================================================================
// 4. Application Bootstrap & Request Handling
// =============================================================================
try {
// Create the server request from PHP globals
$request = \HttpSoft\ServerRequest\ServerRequestCreator::createFromGlobals($_SERVER, $_FILES, $_COOKIE, $_GET, $_POST);
// Build middleware pipeline
/** @var MiddlewareFactory $middlewareFactory */
$middlewareFactory = $container->get(MiddlewareFactory::class);
/** @var EventDispatcherInterface $eventDispatcher */
$eventDispatcher = $container->get(EventDispatcherInterface::class);
$middlewareDispatcher = new MiddlewareDispatcher($middlewareFactory, $eventDispatcher);
$middlewareDispatcher = $middlewareDispatcher->withMiddlewares([
Router::class, // Route requests to controllers
]);
$fallbackHandler = new NotFoundHandler(
$container->get(ResponseFactoryInterface::class)
);
// Create application
$app = new Application(
$middlewareDispatcher,
$eventDispatcher,
$fallbackHandler
);
// Start application
$app->start();
// Handle the request
$response = $app->handle($request);
// Emit the response
$sapiEmitter = new \Yiisoft\PsrEmitter\SapiEmitter(65536);
$sapiEmitter->emit($response);
// Notify that response was emitted
$app->afterEmit($response);
// Shutdown
$app->shutdown();
} catch (Throwable $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
'error' => 'Internal Server Error',
'message' => $e->getMessage(),
'file' => $e->getFile() . ':' . $e->getLine(),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
运行
php -S 127.0.0.1:8002
请求
curl -v http://127.0.0.1:8002/index.php