生命周期
最后更新于:2022-04-01 04:23:49
# 生命周期
前面[关于 Blink](https://github.com/bixuehujin/blink/blob/master/docs/zh-CN/1-1-about-blink.md)部分有过介绍,Blink 不像其他运行在 php-fpm 或者 mod_php 之上的传统 PHP 框架,它的生命周期 也是很不相同的,在使用 Blink 进行应用开发时,我们需要时刻牢记这一点,以减少不必要的困惑。
在 php-fpm 或者 mod_php 中,几乎所有的资源(诸如像对象、数据库链接等)都仅仅存在于一个请求之间,所有的这些资源都会在请求结束时释放。这样的 工作机制对于小规模的应用是没什么问题的,但对于大规模的应用,其所带来的性能开销是不可忽视的。这也是为什么 Blink 存在的原因,在 Blink 中, 我们尽最大努力减少资源的重复申请与释放,获得尽可能高的性能。
为此,Blink 的生命周期会比 php-fpm 或者 mod_php 之类更加复杂,我们在开发 Blink 应用的时候也需要谨慎的处理资源的申请与释放。一个 Blink 应用的有三个阶段,启动阶段、请求处理阶段和退出阶段,下面就每个阶段进行详细介绍:
## 启动阶段
首先,Blink 采用子进程来处理用户请求,每个子进程包含一个 Blink 应用程序(也就是一个 Blink Application 实例),每个应用会随着子进程的创建 而创建。
一旦 Blink 应用创建好后,`$application->bootstrap()` 方法将会被调用,启动应用,在这个方法中主要会做以下几件事情:
1. 设置应用的配置信息,比如默认时区
2. 注册应用服务,如日志、errorHandler等
3. 注册路由
一旦应用启动成功,他就会等待并处理服务器发送过来的请求。
## 请求处理
应用启动之后,就会开始等待处理请求,当有请求到来的时候,`$application->handleRequest()` 将会被调用来处理这个请求,下面是一个请求处理的 过程:
1. 路由分发,通过预先定义好的路由找到对应的控制器
2. 创建对应控制器实例
3. 调用注册好的 Request 中间件
4. 执行控制器对应的 Action
5. 调用注册好的 Response 中间件
6. 结束请求并把数据返回给服务器
与 php-fpm 或 mod_php 相比,Blink 很重要的不同就是一个应用同时处理多个请求。在这多个请求之间,Application 对象及其注册在其之上的服务 都是一直存在并且能够很好的复用,尽量减少了不必要的重新申请资源的开销。也正是因为这个原因,Blink 比 php-fpm 或 mod_php 的性能要高许多。
## 退出阶段
当一个应用(或者子进程)达到了 `maxRequests` 最大处理请求数量的限制或者接收到 `TERM` 信号时,该应用就会自动退出,对应进程内部的所有资源和 数据库链接都会被释放。
对象配置
最后更新于:2022-04-01 04:23:47
# 属性和配置
属性和配置的设计借鉴于 Yii 框架,Blink 实现的 Yii 框架中该特性的子集,如果您对 Yii 的这套理念熟悉,本节只需简单看看即可。如果不熟,您也可以参考 Yii 的[相关文档](https://github.com/yiisoft/yii2/blob/master/docs/guide/concept-configurations.md) 获得更详细的信息。
## 属性
Blink 利用 PHP 的魔术方法实现了增强版的对象属性,通过增强版的属性实现,我们可以在读或者写属性的时候执行一些自定义的代码。Blink 通过 `blink\core\Object` 这个类来提供这一特性,它通过定义类的 *getter* and *setter* 方法来定义这种属性,如果一个类需要这种功能,我们只需要 继承 `blink\core\Object` 类或者他的子类即可。
在下面的实例中,我们通过定义 getLabel() 和 setLabel() 两个方法定义了 label 这个属性。相比 PHP 原生提供的属性,该属性可以在设置它的值时 自动调用 trim 函数,实现自定义代码注入:
~~~
use blink\core\Object;
class Foo extends Object
{
private $_label;
public function getLabel()
{
return $this->_label;
}
public function setLabel($value)
{
$this->_label = trim($value);
}
}
~~~
我们也可以只定义 getter 方法而不定义 setter 方法,这样的属性叫做 read only 属性,如果对该类属性赋值会触发`blink\core\InvalidCallException` 异常;同样的,只定义 setter 方法而不定义 getter 方法的属性叫做 write only 属性,如果尝试读取该类属性值也会触发异常。
除了继承 `blink\core\Object` 这个类,Blink 也提供 `blink\core\ObjectTrait` 和 `blink\core\Configure` 接口,通过使用他们, 我们可以很容易的让第三方库的代码与 Blink 兼容,使用 Blink 提供的 属性和配置 的特性。
## 配置
配置在 Blink 中广泛应用于对象创建和初始化已有对象。一个配置通常包含待创建对象的类名和一系列用于初始化该对象属性的值。下面是一个采用配置创建和 初始化 log 服务的例子:
~~~
$config = [
'class' => 'blink\session\Manager',
'expires' => 3600 * 24 * 15,
'storage' => [
'class' => 'blink\session\FileStorage',
'path' => 'path/to/sessions'
]
];
$session = make($config);
~~~
`make()` 函数是一个采用配置快速创建对象的辅助函数,他先根据配置里包含的类名创建对象,然后再初始化对象的其他属性。
## 配置格式
一个配置的格式如下:
~~~
[
'class' => 'ClassName',
'propertyName' => 'propertyValue',
// 更多的属性
]
~~~
其中:
* class 元素指定待创建对象的类名
* propertyName 元素用于初始化对象对应的属性,key 是属性名称,value 是属性的初始值。注意,只有公开的成员变量和通过 getter 和 setter 定义的属性可以被配置。
理念与架构
最后更新于:2022-04-01 04:23:45
错误与日志处理
最后更新于:2022-04-01 04:23:43
# 日志与错误处理
## 日志
Blink 提供了一个构建在 Monolog 日志库之上、兼容 PSR-3 日志标准的日志服务组件。通过日志服务,我们可以轻松的把各种类型的消息记录到诸如文件、 数据库、邮件等媒介中。
为了记录日志消息,我们首先需要配置日志服务,下面是 Blink 的 seed 项目提供的默认日志配置:
~~~
'log' => [
'class' => 'blink\log\Logger',
'targets' => [
'file' => [
'class' => 'blink\log\StreamTarget',
'enabled' => true,
'stream' => 'php://stderr',
'level' => 'info',
]
],
],
~~~
在这个例子中,我们定义了一个叫做 `file` 的媒介,目的是将所有消息级别小于或等于 *INFO* 的消息写入到 `stderr`中:
另外,获取日志服务和写日志也是很方便的,实例如下:
~~~
// 获取日志服务的实例
$log = app('log');
// emergency 日志类型,系统不可用
$log->emergency('my message');
// alert 日志类型,必要的措施必须马上采取
$log->alert('my message');
// critical 日志类型,危险条件触发
$log->critical('my message');
// error 日志类型,运行时错误
$log->error('my message');
// warning 日志类型,警告
$log->warning('my message');
// notice 日志类型,通常且值得注意的事件
$log->notice('my message');
// info 日志类型,记录感兴趣的事件
$log->info('my message');
// debug 日志类型,记录详细的调试信息
$log->debug('my message');
~~~
## 错误处理
Blink 中,所有的 PHP 错误都会自动转换成 `blink\core\ErrorException` 异常,通过这个特性,我们可以用 try ... catch 来捕获 PHP 错误。
Blink 提供由 `blink\core\ErrorHandler` 类实现的 `errorHandler` 服务来处理 PHP 异常。默认情况下,`errorHandler`会把所有的异常上报给 `log` 服务,我们也可以实现自己的 `errorHandler`,采用不同的方式来处理这些异常,比如上报给 Sentry。
授权与认证
最后更新于:2022-04-01 04:23:40
# 认证与授权
Blink 提供了一套轻量级的*认证授权框架*,通过这套框架我们可以更加方便的在我们的应用中实现认证与授权的系列功能。
在 Blink 中,认证特性是由 `auth` 服务组件来完成的,我们可以通过 `auth()` 辅助函数来获取该服务的实例。为了让`auth` 服务知道如何查找一个 用户并验证其密码的正确性,我们首先需要定义一个 User Identity 类来告诉 `auth` 服务这些信息:
## 定义 User Identity
为了定义一个 User Identity 类,我们需要实现 `blink\auth\Authenticatable` 接口,下面的例子展示了如何利用静态用户数据定义 User Identiry:
~~~
namespace app;
class User extends Object implements Authenticatable
{
public static $users = [
['id' => 1, 'name' => 'user1', 'password' => 'user1'],
['id' => 2, 'name' => 'user2', 'password' => 'user2']
];
public $id;
public $name;
public $password;
/**
* 通过用户的唯一标志查找用户,例如 主键、邮箱
*/
public static function findIdentity($id)
{
if (is_numeric($id)) {
$key = 'id';
$value = $id;
} else if (is_array($id) && isset($id['name'])) {
$key = 'name';
$value = $id['name'];
} else {
throw new InvalidParamException("The param: id is invalid");
}
foreach (static::$users as $user) {
if ($user[$key] == $value) {
return new static($user);
}
}
}
/**
* 返回该用户的 Auth ID,用于存储到 Session 中唯一标志这个用户
*/
public function getAuthId()
{
return $this->id;
}
/**
* 检查用户的密码是否与用户输入相匹配
*/
public function validatePassword($password)
{
return $this->password === $password;
}
}
~~~
User Identity 定义好之后,我们需要配置 `auth` 服务,设置 `model` 属性告诉 `auth` 服务 User Identity 是怎样定义的:
~~~
'auth' => [
'class' => 'blink\auth\Auth',
'model' => 'app\User',
],
~~~
## 用户认证
只要 User Identity 定义并且配置好,我们就可以通过用户输入的用户名和密码来认证用户了,下面是例子:
~~~
$creditials = ['email' => 'foo@bar.com', 'password' => 123];
// 通过给定的用户名和密码进行用户认证
$user = auth()->attempt($creditials);
// 进行用户认证但是不启用 Session
$user = auth()->once($creditials);
~~~
如果采用 `auth()->attempt()` 来认证用户,`auth` 服务会利用 `session` 服务来为认证的用户存储必要的 Session 数据,所以我们需要配置好 `session` 服务以获取期望的结果。
## 用户授权
授权是检查一个用户具有足够权限做某事的过程,Blink 中,该功能由 `blink\http\Request` 类实现,下面是一个简单的例子:
~~~
use blink\core\Object;
use blink\http\Request;
class Controller extends Object
{
public function actionFoo(Request $request)
{
if (!$requst->guest()) {
$user = $requst->user(); // 获取当前授权成功的用户
}
}
}
~~~
目前,Blink 默认采用 `X-Session-Id` Http 头来传输 Session Id。当然,这也是可以配置的,我们可以通过设置`blink\http\Request` 的 `sessionKey` 属性来改变这个行为,关于如何设置该属性,请查看对应类实现的注释。
Session管理
最后更新于:2022-04-01 04:23:38
# Session 管理
Session 允许用户在多个请求中共享数据,在传统 PHP 程序中,我们可以通过 `$_SESSION` 超全局变量来直接获取 Session 数据。但是在 Blink 应用中,`$_SESSION` 超全局变量是没有用也不能被使用的,我们必须通过 session 服务来获取 Sesson 数据。
另外,由 PHP 提供的 Session 相关函数也不能出现在 Blink 应用中,以避免出现一些未知的 Bug 或者未定义的行为。
## Session 服务
Blink 实现了 `blink\session\Manager` 来应用提供 Session 服务的管理,在应用中,我们可以通过 `session()` 辅助方法来获取 Session 服务的 实例,下面是几个展示如何使用 Session 服务的例子:
~~~
// 获取 Session 服务的实例
$manager = session();
// 创建一个新的 Session 对象并保存,返回对象为 \blink\session\Session 类的实例
$session = $manager->put($data);
// 获取新创建 Session 的 Session ID
$sessionId = $session->id;
// 通过 Session ID 获取 Session 数据
$session = $manager->get($sessionId);
// 通过 Session ID 写入新的 Session 数据
$manager->set($sessionId, $newData);
// 通过 Session ID 销毁 Session 数据
$manager->destroy($sessionId);
~~~
在上面的例子中, `put()` 和 `get()` 方法都返回一个 `blink\session\Session` 类的实例。`blink\session\Session` 对象是一个以*键值对*形式 存在的 Session 数据集合,并提供了一些有用的方法来管理 Session 数据。
在 Blink 中,我们也可以实现自定义的 Session 服务,唯一要做的就是实现 `blink\session\Contract` 接口并在配置文件中配置好该服务。
## Session 存储
默认情况下,Blink 采用文件来存储 Session 数据。我们可以通过实现自定义的 Session 存储类来改变这个行为,实现自定义的存储类需要实现 `blink\session\StorageContract` 这个接口,更多可以参考 `blink\session\FileStorage` 类的实现。
依赖注入
最后更新于:2022-04-01 04:23:36
# 依赖注入与服务定位器
依赖注入是控制反转(Inversion of Control,缩写 IoC)的一种实现方法,是面向对象编程的一种设计原则,通过依赖注入, 可以降低类与类之间的耦合,让代码的调试和测试都变得更加简单。
## 依赖注入
Blink 通过 `blink\di\Container` 提供 DI 容器功能,它支持构造方法注入、Setter属性注入和回调注入三种类型的注入方式。
**构造方法注入**
在参数类型提示的帮助下,DI 容器实现了构造方法注入。当容器被用于创建一个新对象时,类型提示会告诉它要依赖什么类或接口。 容器会尝试获取它所依赖的类或接口的实例,然后通过构造器将其注入新的对象。例如:
~~~
class Foo
{
public function __construct(Bar $bar)
{
}
}
$foo = $container->get('Foo');
// 上面的代码等价于:
$bar = new Bar;
$foo = new Foo($bar);
~~~
**Setter 和属性注入**
Setter 和属性注入是通过对象配置提供支持的。当注册一个依赖或创建一个新对象时,你可以提供一个配置,该配置会提供给容器用于通过相应的 Setter 或属性注入依赖。例如:
~~~
use blink\core\Object;
class Foo extends Object
{
public $bar;
private $_qux;
public function getQux()
{
return $this->_qux;
}
public function setQux(Qux $qux)
{
$this->_qux = $qux;
}
}
$container->get('Foo', [], [
'bar' => $container->get('Bar'),
'qux' => $container->get('Qux'),
]);
~~~
**PHP 回调注入**
这种情况下,容器将使用一个注册过的 PHP 回调创建一个类的新实例。回调负责解决依赖并将其恰当地注入新创建的对象。例如:
~~~
$container->set('Foo', function () {
return new Foo(new Bar);
});
$foo = $container->get('Foo');
~~~
## 服务定位器
服务定位器是一个知道如何提供各种应用所需的服务的对象。在服务定位器中,每个服务都只有一个单独的实例(服务都是单例的), 并通过 ID 唯一地标识。用这个 ID 就能从服务定位器中得到这个组件。
Blink 中 `blink\core\ServiceLocator` 实现了服务定位器模式,通过服务定位器,我们可以很容易的配置这些服务, 并且每个服务的实现都是可以替换的,只要它们实现的相同的接口。在 Blink 中,整个 application 其实就是一个服务定位器, 它上面挂载了应用所需要的全部服务,诸如errorHandler服务,日志服务、auth服务等等。
要使用服务定位器,第一步是要注册相关服务。服务可以通过 `blink\core\ServiceLocator::bind()` 方法进行注册。 以下的方法展示了注册组件的不同方法:
~~~
use blink\core\ServiceLocator;
$locator = new ServiceLocator;
// 1\. 通过组件类名
$locator->bind('log', 'blink\log\Logger');
// 2\. 通过配置数组
$locator->bind('log', [
'class' => 'blink\log\Logger',
'targets' => [],
]);
// 3\. 通过匿名函数
$locator->bind('log', function () {
return new blink\log\Logger([]);
});
// 4\. 直接使用类的实例
$locator->bind('log', new blink\log\Logger());
~~~
一旦服务注册成功,你可以任选以下两种方式之一,通过它的 ID 访问它:
~~~
$log = $locator->get('log');
// or
$log = $locator->log;
~~~
## 辅助函数
** 创建对象 **
Blink 提供了构建与 DI 之上的辅助函数 `make($type, $params = [])`,用于快速创建类的实例并进行依赖注入, 通过 make 函数,可以方便的通过对象配置、类名创建实例,如:
~~~
$object = make([
'class' => 'blink\log\Logger',
'prop1' => 'prop2',
]);
// 和
$object = make('blink\log\Logger');
~~~
** 获取服务实例 **
Blink 提供 `app()` 方法快速获取服务实例,如下:
~~~
$log = app('log');
// 等价与
$log = app()->get('log');
~~~
核心组件
最后更新于:2022-04-01 04:23:33
请求处理
最后更新于:2022-04-01 04:23:31
# 请求处理
## 接收输入
Blink 中 `\blink\http\Request` 承载了所有的用户输入,我们可以方便的获取请求头、URL参数、请求数据等信息:
~~~
use \bink\core\Object;
use \bink\http\Request;
class Controller extends Object
{
public function index(Request $request)
{
$type = $request->params->get('type'); // 获取 Query 参数 type
$params = $request->params->all(); // 获取所有 Query 参数
$name = $request->body->get('name'); // 获取 Request Body 的 name 参数
$body = $request->body->all(); // 获取整个 Request Body
}
}
~~~
更多有用的方法请参考 `\blink\http\Request` 的[源代码及注释](https://github.com/bixuehujin/blink/blob/master/src/http/Request.php)。
## 返回数据
Blink 中,Action 方法可以直接返回数据给客户端,支持返回字符串和数组类型:
~~~
use \bink\core\Object;
use \bink\http\Request;
class Controller extends Object
{
public function action1()
{
return 'this is a string'; // 直接返回字符串,原样输出到客户端。
}
public function action2()
{
return [
'name' => 'foo' // 返回数组,json_encode 后输出到客户端
]
}
}
~~~
另外,Request 和 Response 的中间件架构也在计划中,未来会提供更多的方式来对输入输出的数据进行格式化。
路由与控制器
最后更新于:2022-04-01 04:23:29
# 路由与控制器
## 路由
Blink 默认的路由配置位于 `src/http/routes.php` 文件中,该文件返回一个数组,包含应用所有的路由定义。 下面是一个简单的路由配置文件:
~~~
<?php
return [
['GET', '/', function () {
return 'hello world';
}],
['GET', '/foo/bar', function () {
return 'hello foo bar';
}]
];
~~~
路由的定义也支持指定多个 HTTP 请求方法(HTTP Method),实例如下:
~~~
return [
[['GET', 'HEAD'], '/', function () {
// 该路径下的 GET 和 HEAD 请求都将由该函数处理
return 'hello world';
}]
];
~~~
## 带参数的路由
路由的定义也可以携带参数,我们使用`{param}` 的语法来定义一个参数,其中`param`是参数的名称,控制器函数需要按顺序接受 框架传递过来的参数。如下的例子中定义了 type 和 id 两个参数:
~~~
return [
['GET', '/users/{type}/{id}', function ($type, $id) {
// 路由中定义的参数可以直接在控制器函数或方法中获取
}]
];
~~~
上面的例子没有对 type 和 id 参数做任何限制,实际上下面这些 URL 都能通过该路由的校验:
~~~
/users/foo/123
/users/321/bar
/users/foo/bar
~~~
但实际上我们可以只希望 `/users/foo/123` 通过检验,这时我们可以使用正则表达式限制每个参数的值,其对应的语法是`{param:expression}`, 下面的路由定义就符合我们的预期:
~~~
return [
['GET', '/users/{type:[a-zA-Z]+}/{id:\d+}', function ($type, $id) {
// 现在的 type 就限定为字符串, id 限定为整数了
}]
];
~~~
## 控制器
控制器函数除了上文中使用的匿名函数,更常见的是使用类的方法。我们使用 `ClassName@method` 这样的语法指定类方法作为控制器函数,如:
~~~
return [
['GET', '/', '/app/http/controllers/IndexController@index']
];
~~~
该示例中我们采用了类的绝对命名空间,这个看起来会比较繁琐,这是我们可以结合 `src/config/app.php` 中的`controllerNamespace` 配置,采用相对的命名空间格式,简化代码。结合两者,下面示例达到的效果将完全一致:
src/config/app.php
~~~
return [
'controllerNamespace' => '\app\http\controllers',
];
~~~
src/http/routes.php
~~~
return [
['GET', '/', 'IndexController@index']
];
~~~
## 依赖注入
Blink 支持控制器的构造函数和普通方法两种注入方式。通过依赖注入,我们可以很方便的把需要的对象拿来使用,而不用关心这些对象是怎么创建的, 框架本身自然会很好的处理好对象的创建。下面是一个简单的控制器注入案例:
~~~
use blink\core\Object;
use blink\http\Request;
class Controller extends Object
{
/**
* 这里通过构造函数注入 Request 对象
*/
public function __construct(Request $request, $config = [])
{
parent::__construct();
}
/**
* 这里在普通方法中注 Request 对象
*
* @param $id 路由参数 id 的值,注意路由参数需要放在参数列表的前面
* @param $request 注入的 Request 对象
*/
public function index($id, Request $request)
{
}
}
~~~
基础入门
最后更新于:2022-04-01 04:23:27
目录结构
最后更新于:2022-04-01 04:23:24
# Blink 目录结构
Blink 默认应用模板提供一套满足绝大部分应用场景的目录结构,其各部分功能及介绍如下:
~~~
your-app/ 应用根目录
composer.json Composer 配置, 描述应用依赖软件包的信息
src/ 应用源代码
config/
app.php 应用基本配置
server.php Swoole 服务器配置
services.php 应用服务配置
console/ 控制台命令相关
http/ Http 相关
controllers/ 控制器文件夹
routes.php 路由配置
models/ 数据库模型
bootstrap.php
tests/ 应用单元测试或功能测试
runtime/ 应用运行时临时数据,如日志
vendor/ 所有 Composer 安装的软件包
blink Blink 命令行脚本入后
~~~
当然,Blink 提供足够灵活的自定义功能,如果你觉得该目录结构并不满足你的需求,你完全可以配置出任何你需要的目录结构。
安装 Blink
最后更新于:2022-04-01 04:23:22
# 安装Blink
## 1\. 安装 Swoole 扩展
安装前确保您的 PHP 版本大于 php 5.5,之后执行以下命令安装 Swoole:
~~~
$ pecl install swoole
~~~
然后执行命令 `php -m | grep swoole` 确保 Swoole 扩展加载成功。
## 2\. 通过 Composer 安装 Blink
如果你没有安装 Composer, 你可以通过如下方式安装:
~~~
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
~~~
Composer 安装后,你可以通过 Composer 的 create-project 的命令创建项目并安装依赖:
~~~
composer create-project --prefer-dist blink/seed your-app
~~~
## 3\. 确认安装成功
启动 Blink 确认安装成功:
~~~
cd /path/to/your-app
php blink server serve
~~~
成功之后, 打开浏览器在地址栏输入 http://localhost:7788/ , 如果浏览器显示 **"Hello world, Blink"** 的欢迎语那就表示 Blink 已经正常运行。
关于 Blink
最后更新于:2022-04-01 04:23:20
# Blink是什么
Blink 是一个为构建 “long running” 服务而生的 Web 微型高性能框架,它为构建 Web 应用程序提供简洁优雅的API,尽量的减轻我们的常规开发工作。 与此同时,Blink尽可能的保持设计的简洁与可扩展性,允许开发者更加灵活自如的使用。Blink 提供了常用诸如路由、登陆认证、依赖注入、日志处理 等核心组件,让开发者专注于应用本身。
## Blink与其他框架的比较
Blink 与传统 PHP 的 Web 框架非常不同,Blink 的运行不需要 Web 服务器(php-fpm 之于 Nginx, mod_php 之于 Apache)。Blink 本身 就能充当 Web 服务器,直接处理来自客户端的请求。目前我们采用 [Swoole扩展](https://github.com/swoole/swoole-src) 作为底层服务支持。
众所周知,传统的 PHP 应用程序有 Request Startup 和 Request Shutdown 的生命周期,所有的对象在请求后都将销毁,而 Blink 于此不同, Blink 许多对象都能留存与多个请求之间,减少对象反复创建销毁的性能损失。
当然,Blink的潜力不止于此,我们可以发挥更多的想象空间,实现其他框架不能想象或者很难实现的功能。
## Blink适用场景
* 对性能有更加严格要求的场景,通过 Blink 可以获得可观的性能提升
* 实现传统框架因 php-fpm 或 mod_php 的限制而难以实现的功能,如实时聊天
## 环境要求
* PHP 5.5 以上版本
* Swoole 扩展 1.7.19 以上版本
安装与配置
最后更新于:2022-04-01 04:23:17