ソースを参照

Typed view model classes (#5380)

* Typed view model classes
* Add ability to provide a typed view model class to a controller
* Use `::class` instead of string for referring to classes
* Examplified with `stats` and `javascript` controllers / views (more to do)
* Also useful for extensions (my usecase today), which did not have the ability to define own view model attributes before.

* Typo
Alexandre Alapetite 2 年 前
コミット
fe7d9bbcd6

+ 7 - 0
app/Controllers/javascriptController.php

@@ -2,6 +2,13 @@
 
 class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
 
+	/** @var FreshRSS_ViewJavascript */
+	protected $view;
+
+	public function __construct() {
+		parent::__construct(FreshRSS_ViewJavascript::class);
+	}
+
 	public function firstAction(): void {
 		$this->view->_layout(null);
 	}

+ 7 - 0
app/Controllers/statsController.php

@@ -5,6 +5,13 @@
  */
 class FreshRSS_stats_Controller extends FreshRSS_ActionController {
 
+	/** @var FreshRSS_ViewStats */
+	protected $view;
+
+	public function __construct() {
+		parent::__construct(FreshRSS_ViewStats::class);
+	}
+
 	/**
 	 * This action is called before every other action in that class. It is
 	 * the common boilerplate for every action. It is triggered by the

+ 1 - 1
app/FreshRSS.php

@@ -23,7 +23,7 @@ class FreshRSS extends Minz_FrontController {
 			Minz_Session::init('FreshRSS');
 		}
 
-		Minz_ActionController::$viewType = 'FreshRSS_View';
+		Minz_ActionController::$defaultViewType = FreshRSS_View::class;
 
 		FreshRSS_Context::initSystem();
 		if (FreshRSS_Context::$system_conf == null) {

+ 0 - 44
app/Models/View.php

@@ -109,10 +109,6 @@ class FreshRSS_View extends Minz_View {
 	// Form login
 	/** @var int */
 	public $cookie_days;
-	/** @var string */
-	public $nonce;
-	/** @var string */
-	public $salt1;
 
 	// Registration
 	/** @var bool */
@@ -174,44 +170,4 @@ class FreshRSS_View extends Minz_View {
 	/** @var array<string,string> */
 	public $message;
 
-	// Statistics
-	/** @var float */
-	public $average;
-	/** @var float */
-	public $averageDayOfWeek;
-	/** @var float */
-	public $averageHour;
-	/** @var float */
-	public $averageMonth;
-	/** @var array<string> */
-	public $days;
-	/** @var array<string,array<int,int|string>> */
-	public $entryByCategory;
-	/** @var array<int,int> */
-	public $entryCount;
-	/** @var array<string,array<int,int|string>> */
-	public $feedByCategory;
-	/** @var array<int, string> */
-	public $hours24Labels;
-	/** @var array<string,array<int,array<string,int|string>>> */
-	public $idleFeeds;
-	/** @var array<int,string> */
-	public $last30DaysLabel;
-	/** @var array<int,string> */
-	public $last30DaysLabels;
-	/** @var array<string,string> */
-	public $months;
-	/** @var array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false */
-	public $repartition;
-	/** @var array{'main_stream':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false,'all_feeds':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false} */
-	public $repartitions;
-	/** @var array<int,int> */
-	public $repartitionDayOfWeek;
-	/** @var array<string,int>|array<int,int> */
-	public $repartitionHour;
-	/** @var array<int,int> */
-	public $repartitionMonth;
-	/** @var array<array{'id':int,'name':string,'category':string,'count':int}> */
-	public $topFeed;
-
 }

+ 16 - 0
app/Models/ViewJavascript.php

@@ -0,0 +1,16 @@
+<?php
+
+final class FreshRSS_ViewJavascript extends FreshRSS_View {
+
+	/** @var array<FreshRSS_Category> */
+	public $categories;
+	/** @var array<FreshRSS_Feed> */
+	public $feeds;
+	/** @var array<FreshRSS_Tag> */
+	public $tags;
+
+	/** @var string */
+	public $nonce;
+	/** @var string */
+	public $salt1;
+}

+ 55 - 0
app/Models/ViewStats.php

@@ -0,0 +1,55 @@
+<?php
+
+final class FreshRSS_ViewStats extends FreshRSS_View {
+
+	/** @var FreshRSS_Category|null */
+	public $default_category;
+	/** @var array<FreshRSS_Category> */
+	public $categories;
+	/** @var FreshRSS_Feed|null */
+	public $feed;
+	/** @var array<FreshRSS_Feed> */
+	public $feeds;
+	/** @var bool */
+	public $displaySlider;
+
+	/** @var float */
+	public $average;
+	/** @var float */
+	public $averageDayOfWeek;
+	/** @var float */
+	public $averageHour;
+	/** @var float */
+	public $averageMonth;
+	/** @var array<string> */
+	public $days;
+	/** @var array<string,array<int,int|string>> */
+	public $entryByCategory;
+	/** @var array<int,int> */
+	public $entryCount;
+	/** @var array<string,array<int,int|string>> */
+	public $feedByCategory;
+	/** @var array<int, string> */
+	public $hours24Labels;
+	/** @var array<string,array<int,array<string,int|string>>> */
+	public $idleFeeds;
+	/** @var array<int,string> */
+	public $last30DaysLabel;
+	/** @var array<int,string> */
+	public $last30DaysLabels;
+	/** @var array<string,string> */
+	public $months;
+	/** @var array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false */
+	public $repartition;
+	/** @var array{'main_stream':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false,'all_feeds':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false} */
+	public $repartitions;
+	/** @var array<int,int> */
+	public $repartitionDayOfWeek;
+	/** @var array<string,int>|array<int,int> */
+	public $repartitionHour;
+	/** @var array<int,int> */
+	public $repartitionMonth;
+	/** @var array<array{'id':int,'name':string,'category':string,'count':int}> */
+	public $topFeed;
+
+}

+ 1 - 1
app/views/javascript/actualize.phtml

@@ -1,5 +1,5 @@
 <?php
-/** @var FreshRSS_View $this */
+/** @var FreshRSS_ViewJavascript $this */
 
 $categories = [];
 foreach ($this->categories as $category) {

+ 2 - 1
app/views/javascript/nbUnreadsPerFeed.phtml

@@ -1,5 +1,6 @@
-<?php /** @var FreshRSS_View $this */ ?>
 <?php
+/** @var FreshRSS_ViewJavascript $this */
+
 $result = array(
 	'feeds' => array(),
 	'tags' => array(),

+ 1 - 1
app/views/javascript/nonce.phtml

@@ -1,3 +1,3 @@
-<?php /** @var FreshRSS_View $this */ ?>
 <?php
+/** @var FreshRSS_ViewJavascript $this */
 echo json_encode(array('salt1' => $this->salt1, 'nonce' => $this->nonce));

+ 1 - 1
app/views/stats/idle.phtml

@@ -1,5 +1,5 @@
 <?php
-	/** @var FreshRSS_View $this */
+	/** @var FreshRSS_ViewStats $this */
 	$this->partial('aside_subscription');
 ?>
 

+ 1 - 1
app/views/stats/index.phtml

@@ -1,5 +1,5 @@
 <?php
-	/** @var FreshRSS_View $this */
+	/** @var FreshRSS_ViewStats $this */
 	$this->partial('aside_subscription');
 ?>
 

+ 1 - 1
app/views/stats/repartition.phtml

@@ -1,5 +1,5 @@
 <?php
-	/** @var FreshRSS_View $this */
+	/** @var FreshRSS_ViewStats $this */
 	$this->partial('aside_subscription');
 ?>
 

+ 16 - 5
lib/Minz/ActionController.php

@@ -21,16 +21,27 @@ class Minz_ActionController {
 	protected $view;
 
 	/**
-	 * Gives the possibility to override the default View type.
+	 * Gives the possibility to override the default view model type.
 	 * @var class-string
+	 * @deprecated Use constructor with view type instead
 	 */
-	public static $viewType = 'Minz_View';
+	public static $defaultViewType = Minz_View::class;
 
-	public function __construct () {
+	/**
+	 * @phpstan-param class-string|'' $viewType
+	 * @param string $viewType Name of the class (inheriting from Minz_View) to use for the view model
+	 */
+	public function __construct(string $viewType = '') {
 		$this->csp_policies = self::$csp_default;
 		$view = null;
-		if (class_exists(self::$viewType)) {
-			$view = new self::$viewType();
+		if ($viewType !== '' && class_exists($viewType)) {
+			$view = new $viewType();
+			if (!($view instanceof Minz_View)) {
+				$view = null;
+			}
+		}
+		if ($view === null && class_exists(self::$defaultViewType)) {
+			$view = new self::$defaultViewType();
 			if (!($view instanceof Minz_View)) {
 				$view = null;
 			}

+ 10 - 10
tests/app/Models/UserQueryTest.php

@@ -30,13 +30,13 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 	public function test__construct_whenCategoryQuery_storesCategoryParameters(): void {
 		$category_name = 'some category name';
 		/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
-		$cat = $this->createMock('FreshRSS_Category');
+		$cat = $this->createMock(FreshRSS_Category::class);
 		$cat->expects($this->atLeastOnce())
 			->method('name')
 			->withAnyParameters()
 			->willReturn($category_name);
 		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
-		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
+		$cat_dao = $this->createMock(FreshRSS_CategoryDAO::class);
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->withAnyParameters()
@@ -58,13 +58,13 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 	public function test__construct_whenFeedQuery_storesFeedParameters(): void {
 		$feed_name = 'some feed name';
 		/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
-		$feed = $this->createMock('FreshRSS_Feed');
+		$feed = $this->createMock(FreshRSS_Feed::class);
 		$feed->expects($this->atLeastOnce())
 			->method('name')
 			->withAnyParameters()
 			->willReturn($feed_name);
 		/** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
-		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
+		$feed_dao = $this->createMock(FreshRSS_FeedDAO::class);
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->withAnyParameters()
@@ -164,9 +164,9 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 
 	public function testIsDeprecated_whenCategoryExists_returnFalse(): void {
 		/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
-		$cat = $this->createMock('FreshRSS_Category');
+		$cat = $this->createMock(FreshRSS_Category::class);
 		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
-		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
+		$cat_dao = $this->createMock(FreshRSS_CategoryDAO::class);
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->withAnyParameters()
@@ -178,7 +178,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 
 	public function testIsDeprecated_whenCategoryDoesNotExist_returnTrue(): void {
 		/** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
-		$cat_dao = $this->createMock('FreshRSS_CategoryDAO');
+		$cat_dao = $this->createMock(FreshRSS_CategoryDAO::class);
 		$cat_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->withAnyParameters()
@@ -190,9 +190,9 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 
 	public function testIsDeprecated_whenFeedExists_returnFalse(): void {
 		/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
-		$feed = $this->createMock('FreshRSS_Feed');
+		$feed = $this->createMock(FreshRSS_Feed::class);
 		/** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
-		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
+		$feed_dao = $this->createMock(FreshRSS_FeedDAO::class);
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->withAnyParameters()
@@ -204,7 +204,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
 
 	public function testIsDeprecated_whenFeedDoesNotExist_returnTrue(): void {
 		/** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
-		$feed_dao = $this->createMock('FreshRSS_FeedDAO');
+		$feed_dao = $this->createMock(FreshRSS_FeedDAO::class);
 		$feed_dao->expects($this->atLeastOnce())
 			->method('searchById')
 			->withAnyParameters()