MST ライブラリの初期から Agimvc の時代までの 2 年を経て、厳密に言うと 2 年以上かかりました。 2011 年の Agimvc 3.2 バージョン以降に作成されました。
php5 のことは忘れてください、忘れてください!
これは主に Java のプロジェクトメカニズムを利用しています。
X-ERP という名前のプロジェクトがあるとします。このプロジェクトの名前空間は xErp に基づいています。次に、X-ERP コードを再利用したいと考えています。いくつかのメソッドがあります:
新しいプロジェクトの src ディレクトリに xErp を置きます。新しいプロジェクトの src ディレクトリの下に、srcMyErp と srcxErp があります。
新しいプロジェクトでは、クラスは xErp クラスから直接継承できます
namespace MyErp\Model;class User extends xErp\Model\User {}
xErp のビュー コンポーネント (View、Component、Layout) を再利用したい場合は、ディレクトリを手動で登録するだけで済みます。
$web->component->setDirs([ 'xErpView' => [$app->src('xErp/View'), 200, Component::VIEW], 'xErpComponent' => [$app->src('xErp/Component'), 200],]);
新しいプロジェクトの場合、古いプロジェクトのディレクトリを登録します。
$app->getLoader()->setDirs([ 'xErpSrc' => ['any_path', 200], 'xErpHelper' => ['any_path/Helper', 200, Loader::HELPER],]);
KeLoader は、プロジェクトのクラスとヘルパー (関数) をロードするために使用されます。
KeWebComponent は、プロジェクトのビュー、ウィジェット、レイアウトを読み込むために使用されます。
複数のディレクトリの登録と複数のディレクトリの検索をサポートします。重量値の制御が可能になります。
Composer をロードするには、プロジェクトの bootstrap.php ファイルにロードするだけです。
これは説明するのが簡単ではない質問であり、私が 2012 年から考え続けていることです。
従来の配列ベースの構成は、非常に実用的ではありますが、コード レベルでは使いにくく、特にデータの包括性を判断する場合には多くの判断が必要です。
今度は、アプリのグローバル アプリケーション構成など、クラス属性の使用に完全に切り替えて、構成にクラス属性を直接使用します。
namespace MyApp;class App extends Ke\App { protected $name = 'MyApp'; protected $salt = 'xxxxxxxxxxxxxx';}
今回は再カプセル化され、App、Web、Console の 3 つの主要なエントランスが実際のアプリケーションで継承およびリロードできるようになります。
コア クラス ライブラリはデフォルトの動作スタイルを提供しますが、この動作スタイルをリファクタリングする入り口を完全に閉じるわけではありません。
コア クラス ライブラリの必要な隠し属性と特性は、オブジェクト インスタンスを通じて隠蔽されます。継承クラスは、実装するロジックを考慮するだけで済みます。
たとえば、デフォルトのコントローラーはアクションが存在しない場合に例外をスローしますが、ユーザーは onMissing メソッドをオーバーロードして自分で制御できます:
class Index extends Controller { protected function onMissing(string $action) { // 原来的做法 // throw new \Exception("Action {$action} not found!"); }}
実は私は個人的には好きではありませんWordpress のアクションとフィルターの一般的なメカニズム (文字列を介してイベントを宣言して呼び出す) は、一方では高パフォーマンスのアプローチではありません。第 2 に、この種のイベント フックの大規模な普及は重要です。開発者にとっては大きな問題です。イベント テーブルが多すぎて覚えきれないのです。
各主要なコア クラス (アプリ、Web、コンソール、コマンド、コントローラー、レンダラー) には on という接頭辞が付いたイベント メソッドが用意されており、これらのメソッドはすべてソース コード レベルでコード エディターによって使用できます。
モデルレイヤーの名前付けは、以前の Ror スタイル (before*、after*) を維持します。
このアプローチの目的と中心となるアイデアは、オブジェクト指向のアプローチを通じてオープン、オープン、完全にオープンになることです (コードを認識可能にし、プロンプトを出しやすくする)。そのため、フレームワークは他人のフレームワークではなく、開発者自身のフレームワークになることができます。
やり直しの目的は、完全に簡素化し、単純化し、さらにシンプルにすることです。何も考えずに、ビジネス ロジックを書くだけです。
反映されたコマンドの使い方は以下の通りです:
class MyCmd extends ReflectionCommand{ protected static $commandName = 'my_cmd'; protected static $commandDescription = ''; // define fields: type|require|default|field|shortcut // types: string|integer|double|bool|dir|file|realpath|json|concat|dirs|files|any... /** * 对应执行命令时候的第一个参数,如:php ke.php my_cmd <name> * 该参数为必须的参数 * * @var string * @type string * @require true * @field 1 */ protected $name = ''; /** * 对应执行命令时候的第一个参数,如:php ke.php my_cmd <name> -s=<source> * 该参数不是必须的 * * @var string * @type string * @field s */ protected $source = ''; /** * @var string * @type string * @field s */ protected $source = null; protected $tip = 'Creating model'; protected function onConstruct($argv = null) { // 命令实例化接口 } protected function onPrepare($argv = null) { // 命令执行前的预备阶段 } protected function onExecute($argv = null) { // 命令执行时的实际入口 // 这个方法是必须的 }}
上記クラスの属性宣言により、コマンド実行時に実行されるパラメータに自動対応し、自動的にフィルタリングと値の型変換を行います。一般的に、実際のビジネスロジックをプログラムしていれば、余計なことは考えません。
kephp 自体は、GitExport などの多くのコマンドを提供します。このコマンドは主に、現在のプロジェクトと以前のバージョン (または指定されたバージョン) の更新されたファイルをエクスポートし、指定されたディレクトリに配置するために使用されます。 ke.php git_export 。
如果用户希望在这里基础上,增加一些新的功能,可以在项目添加:src/Cmd/GitExport
namespace Cmd\GitExport;class GitExport extends \Ke\Cli\Cmd\GitExport {}
那么同样在执行这个命令时候,会优先加载用户设定的类,并且并不会和全局的ClassLoader产生冲突。
除了使用命令的方式调用,还可以通过实例化一个Command来调用(可以参考\Ke\Cli\Cmd\Add)指令。
$cmd = new UpdateModel([null, 'User']);$cmd->execute();
现在对于WebRouter的做法,有很多新的做法,比如:$web->get('/post/:id/edit'),这种,还有MiddleWare的模式等。
请恕我鲁钝,无法接受那些五花八门的东西,我坚持一个原则:一个项目的路由分发,应该只在一个地方进行统一、集中的配置,而不应该到处都能分发,不然要找现在到底是哪个分发器起作用,或者是去找这个action下一步又跳转去哪个action去了。
这里和过往MST Library、Agimvc里面所坚持的编程方针有关。
因为Model是具有数据、具有行为能力(方法)的实体,所以业务逻辑应该作为Model的接口行为,而Controller\Action层面,则应该只包含最简单的流程控制。所有涉及数据实体的操作,应该一律放在Model层(同样的道理,所有展现相关的,应该放在View层),Model层和View层中间,使用Helper进行无状态衔接。
新版本的Router配置仍然采用数组的方式:
<?php// config/routes.php$router->routes = [ // 根空间分发 '*' => [ ], // 匹配所有hello/*的请求 'hello' => [ 'namespace' => 'hi', 'mappings' => [ ['world/{name}', 'controller#action', ['name' => '[a-z0-9_-]+']], ] ], 'admin' => [ 'path' => 'management', // 允许通过path字段,对前缀进行修改,匹配management/*,而不会匹配admin // 这里没有指定命名空间,他会使用节点名admin,注意不是path的management ],];
上述所示,是传统路由器的分发模式,每个节点,代表着的是一个基础的前缀路径(同时也是命名空间)。每个节点都可以配置相关的 mappings字段。
如果没指定,则按照 controller/action/tail的方式进行匹配。
新版本增加了两种匹配的模式:
一、controller分发模式
<?php// config/routes.php$router->routes = [ // 根空间分发 'user' => [ 'controller' => 'user', ],];
这种分发模式,会将所有user/*的请求,绑定到user控制器,user/profile => { controller: user, action: profile }。
二、class分发模式
<?php// config/routes.php$router->routes = [ // 根空间分发 'assets' => [ 'class' => MyApp\Controller\Asset::class, 'action' => 'output', ],];
这种模式下,会 将所有assets/*请求都转发到Asset这个类,注意这个类必须继承自Controller。
并且指定了默认的action,除非指定了mappings,否则所有的请求都会使用output这个action。
controller和class模式,会使用action/tail的方式进行默认的匹配。区别于传统模式。
默认行为中:
当然,如上述,这只是一个保守的默认行为而已,所有命名风格,允许用户继承Web,并进行重载相关的方法。但是前后一致性的问题,需要自行处理。
默认行为中, namespace/controller#action为字符表达格式。
假定有动作:login,如果只是GET请求,只会关联方法 login,如果是post请求,则会进一步关联 login和 post_login,两者共享一个当前的controller实例。
默认行为,如果不存在login方法,会抛出异常。
action输出,有三种方法:
在 login和 post_login的时候,
实际上核心判断的标准是,检测当前的 $web->isRender(),如果任何操作出发了Wen渲染,则其他操作都不会再生效。
默认的输出,可以通过重载Controller的 defaultReturn方法进行控制。
Router匹配到的特定变量,会作为action执行时的参数,比如:
<?php// config/routes.php$router->routes = [ // 根空间分发 'assets' => [ 'class' => MyApp\Controller\Asset::class, 'mappings' => [ ['{path}', '#output', ['path' => '.*']], ], ],];
上述的匹配,默认行为,会匹配:
namespace MyApp\Controller;class Asset extends Controller { public function output($path = '') { }}
如果希望定制传入的参数,可以重载Controller的 getActionArgs(array $params)方法的返回结果。
在实际项目中的 index.php
<?phprequire '../bootstrap.php';$web = new \Ke\Web\Web();$web->dispatch();
如果属于自己定制的Web,可以改为 new \MyApp\Web()。
而用户不必特别去global标识$web,通过: \MyApp\Web::getWeb(),或者 \Ke\Web\Web::getWeb(),都可以获取回这个实例(\Ke\Cli\Console和\Ke\App都是类似的道理),为了保证开放性,没有严格限制Web和Console的重复创建实例的问题,App则限制了重复实例。
Web部分的结构:
Web -> Router -> Controller -> Context -> Renderer
变量的传递,不需要在调用 $this->view()的方法中传递,只需要将变量绑定到Controller上(注意要public),就会自动传递到Context上。
Context是渲染视图时的上下文变量环境,通过Controller的$this绑定的变量,被视作超级全局变量,直接绑定到Context上,比如:
class User extends Controller { protected $tempVar = 'temp'; // 这个属性不会传递到Context中 public $topWidget = 'user_bar'; // 这个变量会传递到Context public function index() { $this->user = User::loadCache(1); // 这个也会传递到Context }}
相应的,在 user/indexview中:
<?php$this->topWidget; // user_bar$this->user; // User::loadCache(1);?>
在View环境下,$this指向的就是当前Web渲染时的Context实例。
在View环境下,拥有两个变量: $this和 $web,$this对应的是Context,$web则对应的是Web的实例。
而在Layout中,会多一个变量: $content,这个是加载视图时获取到的内容,你只要 print $content ?? ''。
在Component中,情况就会复杂一些,但也只是复杂一些,假定在view中,我加载了一个component:
<?php$this->component('user_bar', ['user' => $this->user]);
那么在 user_bar这个component中:
<?php$this->user; // 是Context中的变量,User::loadCache(1);$user; // 则是加载这个component时,传递过来的局部变量,他只在当前的component中有效。
component的加载,还允许增加一个layout,这是一个很变态的做法,很多时候,我们的前端都会将一个区块的代码,做成一个可复用的module,比如:
<div class="module user-module"> <div class="title"> title </div> <div class="body"> body </div></div>
恩,很好很规整,那么我们只要把上述的代码稍微修改,并放入src/Component/layout/user_module.phtml中:
<div class="module user-module"> <div class="title"> <?php print $title ?? '' ?> </div> <div class="body"> <?php print $content ?? '' ?> </div></div>
然后在需要的时候调用
<?php$this->component('user_profile', 'user_module', ['title' => '个人资料']);
而且如果你想的,可以进一步的将他封装为一个函数,反正是你的自由。
最后说一点:
$web->setContext(new MyApp\Helper\UserContext);
你懂的。
一、重新调整Columns的生成,尽量不破坏原Columns的内容。
二、dbColumns()接口,为数据库的声明,请不要随意修改,请直接修改 protected static $columns属性
三、Columns声明增加update和create的区别:
namespace MyApp\Model;class User extends \Ke\Model { protected static $columns = [ 'email' => ['label' => '邮箱', 'require' => 1, 'unique' => 1, 'min' => 5, 'max' => 128, 'email' => true], 'name' => [ 'label' => '姓名', self::ON_UPDATE => [ 'require' => 1, 'min' => 3 ], // 嗯这样的逻辑比较奇葩,只是为了展示特性 ], 'created_at' => [self::ON_CREATE => 'now'], 'updated_at' => [self::ON_UPDATE => 'now'], 'saved_at' => [self::ON_SAVE => 'now'], // create update都会触发 ];}
四、重新调整Model缓存的问题,增加相应的接口: onCreateCache, onUpdateCache, onSaveCache, onDeleteCache。可缓存Model,要求必须有主键的Model才可以。加载缓存: Mode::loadCache(1)。 before*和 after*系列接口就不用介绍了。
五、数据更新前会做差异化比较。
六、去掉很多没用的东西、代码优化。
这个是专门用来针对缓存数据的模型。呃,如果不能理解为什么会有这样的东西,这段就跳过吧。
prepareData,接口必须实现,用于实现一个缓存数据模型的装填和准备阶段。
onCreate, onUpdate, onSave, onDelete接口。
新版本的查询构造器,都改用 Query实现,可以通过数组的方式构建。
$query = (new Query())->select('id', 'name', 'email', ['login_id', '...'])->from('user');$query->where('status', '=', 1)->addWhere([ ['id', 'in', 1,2,[4,5],6,7,8,9,10], 'OR', [ ['name', 'like', 'jan%'], 'AND', ['email', '=', 'janpoem@163.com'], ]]);$query->join(QueryBuilder::LEFT_JOIN, 'user_log.user_id', 'user.id');// 输出调试的sql,这里会让变量放入sql字符串中,方便直接调试。$query->sql();$query->find(); // 查全部$query->findOne(); // 查一条$query->count(); // 查记录数$query->column('id'); // 取id这一列$query->columnOne('id');
如果希望绑定到Model实例上:
User::query()->where(...); // 通过Model调用的,可以不需要 写from
class User extends Model { protected static $queries = [ 'logs' => [ 'select' => 'tb1.*', 'from' => 'user', // 这个其实不是必须写的,并且表索引会自动关联tb1 'join' => [ [QueryBuilder::LEFT_JOIN, 'user_log.user_id', 'tb1.id'] ], 'order' => ['status', 'updated_at', -1], // -1是DESC,1是ASC ] ];}
假定我需要复用这个查询:
User::query('logs')->where('tb1.id', '>', 100)->limit(20); // 这里会clone一个新的查询,而不会污染原来的查询构造。
所有生成Uri的地方,都使用了这个类,比如: $web->uri('hello/world');
注意,这里要生成uri的时候,需要使用 $uri->newUri()的方法。这个方法会clone一个新的实例。当然Web和Asset的代码,已经进行相关的封装,一般用户不用关心这个问题。
假定你的网站根目录为: /my_app,那么注意传参数时候的区别:
$baseUri->newUri('hello/world'); // => /my_app/hello/world/,注意,如果不是xxx.yyy的结尾,会强制补/$baseUri->newUri('/good'); // => /good/$baseUri->newUri('.././good'); // => /my_app/../good/,注意这里不会做../的目录缩进的处理,而交给服务器吧。但是会去掉./
同样的,查询字符串也会自动合并:
$baseUri->newUri('what?id=1', ['id' => 2]); // 最终生成是:/my_app/what/?id=2
这是文件Media Type的匹配,本来是希望作为一个静态类来使用的,谁没事搞几个版本的Mime呢?不过为了保持这个版本的风格一致,还是使用了对象实例的方式。
哦哦,根据当前项目配置的数据库自动生成Model文件,如果文件存在,则更新。很爽。
$content = $ob->getFunctionBuffer(null, function() { echo 'hello world'; var_dump('good!');});$content = $ob->getImportBuffer(null, 'test.php', ['var1' => 123]);
嗯……
当前为先行版本,包含实现了主要特性,不过不要放入实际项目中,还有一些东西没做。