[Google Analytics API] 日付別のPVを取得したい

dimensionをga:dateにして、metricsをga:pageviewsにする。
$data[“rows”][0][“date”][“pv”] みたいな感じで返ってくるので、一度、rowsを取得してから、foreachで回すとdateとpvを取得できる。

$start_day = date("Y-m-d",strtotime("-1 week"));
$end_day = date("Y-m-d");


$data = $analytics->data_ga->get(
  'ga:' . $profile,
  $start_day,
  $end_day,
  'ga:pageviews',
 array(
   "dimensions" => 'ga:date',
   "metrics" => 'ga:pageviews',
   "sort" => 'ga:date',
  )
);
$result = $data -> rows;
foreach($result as $key => $value){
 echo  '日付:' . $value[0]. ' PV:' . $value[1] . "<br>";
}

うん、これをchart.jsで表示したい。できれば、ユーザがformで日付(開始日、終了日)を入力して、それを表示。
PHPからJavascriptにjsonで渡して表示でOKだと思うんだが、Chart.jsの書き方忘れたから復習からだ。

[Google Analytics API] ユーザが範囲指定した値のデータを表示する

– ユーザが範囲指定した期間のPV、セッション、上位閲覧ページを表示したい。
– UIとしては以下のように、開始日と終了日のDatepickerがある。

### form
まず、formはgetメソッドで作成する

<form action="" method="get">
      <div class="search">
      <div class="form-group form-inline ">
        <label for="" class="">注文日</label>
        <input type="text" class="form-control" name="datepicker_s" placeholder="">
        <label for="" class=""> ~ </label>
        <input type="text" class="form-control" name="datepicker_e" placeholder="">
        <button type="submit" class="btn">検索</button>
      </div>
      </div>
    </form>

### js
– 開始日は終了日より以前、終了日は開始日より以降 をjsのon changeで制御する
– 開始日、終了日共にmaxDateは今日までとする

<script
  src="https://code.jquery.com/jquery-3.5.1.js"
  integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc="
  crossorigin="anonymous"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
  	<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/i18n/jquery.ui.datepicker-ja.min.js"></script>
  	<script>
    $(function(){
      var format = 'yy-mm-dd';

      var start = $("[name=datepicker_s]").datepicker({
        dateFormat: 'yy-mm-dd',
        maxDate: '0'
      }).on("change", function(){
        end.datepicker("option", "minDate", getDate(this));
      });

      var end = $("[name=datepicker_e]").datepicker({
        dateFormat: 'yy-mm-dd',
        maxDate: '0',
      }).on("change", function(){
        start.datepicker("option", "maxDate", getDate(this));
      });

      function getDate(element){
        var date;
        try {
          date = $.datepicker.parseDate(format, element.value);
        } catch(error){
          date = null;
        }
        return date;
      }
    });
  </script>

### php
– (1)開始日、終了日入力なし、(2)開始日のみ入力、(3)終了日のみ入力、(4)開始日、終了日入力あり の4パターンを書いてやれば良い
– 「終了日のみ入力」の際に、開始日をいつに設定するかは決めの問題。便宜上、下では終了日の一週間前にしている

if(isset($_GET['datepicker_s']) && $_GET['datepicker_s'] !== ""){
  $start_day = $_GET['datepicker_s'];

  if(isset($_GET['datepicker_e']) && $_GET['datepicker_e'] !== ""){
    $end_day = $_GET['datepicker_e'];
  } else {
    $end_day = date("Y-m-d");
  }

} else {
  if(isset($_GET['datepicker_e']) && $_GET['datepicker_e'] !== ""){
    $start_day = date("Y-m-d",strtotime("-1 week",  strtotime($_GET['datepicker_e'] . " 00:00:00")));
    $end_day = $_GET['datepicker_e'];
  } else {
    $start_day = date("Y-m-d",strtotime("-1 week"));
    $end_day = date("Y-m-d");
  }
}

$data = $analytics->data_ga->get(
  'ga:' . $profile,
  $start_day,
  $end_day,
  'ga:pageviews',
);
$pv = $data -> rows[0][0];

この分岐処理は、ハマると時間がかかるんだよなー

[Google Analytics API] タイトルとURLをPV順にソートして表示

この辺を押さえておけば大丈夫そう
訪問数: ga:sessions
合計PV: ga:pageviews
平均閲覧ページ数: ga:pageviewsPerSession
平均滞在時間: ga:avgSessionDuration
直帰率: ga:bounceRate

タイトル: ga:pageTitle
パス: ga:pagePath

require_once 'google-api-php-client/src/Google/autoload.php';
$service_account_email = '*';
$key = file_get_contents('analytics-*-*.p12');
$profile = '*';

$client = new Google_Client();
$analytics = new Google_Service_Analytics($client);

$cred = new Google_Auth_AssertionCredentials(
	$service_account_email,
	array(Google_Service_Analytics::ANALYTICS_READONLY),
	$key
);

$client->setAssertionCredentials($cred);
if($client->getAuth()->isAccessTokenExpired()){
	$client->getAuth()->refreshTokenWithAssertion($cred);
}

$pv = $analytics->data_ga->get(
	'ga:' . $profile,
	'7daysAgo',
	'yesterday',
	'ga:pageviews',
);
echo $pv -> rows[0][0] . "\n";


$result = $analytics->data_ga->get(
	'ga:' . $profile,
	'7daysAgo',
	'yesterday',
	'ga:pageviews',
	array(
		// "dimensions" => 'ga:pageTitle',
		"dimensions" => 'ga:pagePath',
		"sort" => '-ga:pageviews',
		"max-results" => '10',
	)
);

$data = $result->rows;

foreach($data as $key => $row){
	echo  ($key + 1) . ':' . $row[0]. ' ' . $row[1] . "\n";
}

ちょっと処理に時間がかかるので、テーブル作ってトップページの値はCronでデータベースに格納しておくか。
あとは日付指定のところをDatepicker & Postで作りたい。
todayは0daysAgo, yesterdayは’1daysAgo’でもいけるみたいだけど、[0-9]+(daysAgo)は9日前までしかできないから、YYYY-MM-DDの方が良さそうですね。
[0-9]{4}-[0-9]{2}-[0-9]{2}|today|yesterday|[0-9]+(daysAgo)

https://developers.google.com/analytics/devguides/reporting/core/v3/reference?hl=ja

[webpack 5.6.0]webpack-dev-server^3.11.0でMODULE_NOT_FOUND

$ npm run start

> front@1.0.0 start /home/vagrant/dev/front
> webpack-dev-server

internal/modules/cjs/loader.js:883
throw err;
^

Error: Cannot find module ‘webpack-cli/bin/config-yargs’
Require stack:
– /home/vagrant/dev/front/node_modules/webpack-dev-server/bin/webpack-dev-server.js
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:880:15)
at Function.Module._load (internal/modules/cjs/loader.js:725:27)
at Module.require (internal/modules/cjs/loader.js:952:19)
at require (internal/modules/cjs/helpers.js:88:18)
at Object. (/home/vagrant/dev/front/node_modules/webpack-dev-server/bin/webpack-dev-server.js:65:1)
at Module._compile (internal/modules/cjs/loader.js:1063:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Module.load (internal/modules/cjs/loader.js:928:32)
at Function.Module._load (internal/modules/cjs/loader.js:769:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12) {
code: ‘MODULE_NOT_FOUND’,
requireStack: [
‘/home/vagrant/dev/front/node_modules/webpack-dev-server/bin/webpack-dev-server.js’
]
}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! front@1.0.0 start: `webpack-dev-server`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the front@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! /home/vagrant/.npm/_logs/2020-11-20T05_45_43_741Z-debug.log

こちらのissueを参考に修正する
https://github.com/webpack/webpack-dev-server/issues/2759

package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack serve --mode development --env development"
  },

$ npm run start

これで上手くいきます。

[JavaScript] 配列・二次元配列・連想配列の違い

– letは再代入不可、varは再代入可

### 普通の配列処理

<script>
		let fruits = ['りんご', 'バナナ', 'みかん']

		console.log(fruits[0])

		fruits.push('なし')

		fruits.forEach(function(item,index, array){
			console.log(item)
		})
	</script>

### 二次元配列

	<script>
		let fruits = [
			["新橋", "品川", "東京"],
			["札幌", "釧路", "函館"]
		]
		console.log(fruits[0][2])
		// fruits.forEach(function(value){
		// 	console.log(value);
		// })
		fruits.forEach(function(value){
			value.forEach(function(key){
				console.log(key);
			})
		})
	</script>

ここまではわかる。続いて、連想配列

### 連想配列
セミコロンで繋げる

	<script>
		let ary = { 
			tokyoto: ['tokyo','shinagawa','shinjyuku'],
			hokkaido: ['sapporo','kushiro','tomato'] }
		console.log(ary['tokyoto'])
	</script>
let ary = { 
			tokyoto: ['tokyo','shinagawa','shinjyuku'],
			hokkaido: ['sapporo','kushiro','tomato'] }

		ary.tokyoto.forEach(function(value){
			console.log(value);
		})

OK, 連想配列と多次元配列の扱いは理解した。

[Laravel8.15.0]Jetstreamによる権限付与

middlewareでユーザーroleを判定して出し分けようかと考えていたが、Laravel8系から実装された新機能として、Jetstreamチーム版でユーザの役割・権限を簡単に設定できるそうなので、それを使ってみて、Jetstreamが良いかMiddleware制御が良いか判断したい。

jetstream付きでインストールするにはインストーラーでインストールする必要がある為、

$ composer global require laravel/installer
$ export PATH=”/home/vagrant/.config/composer/vendor/bin:$PATH”
$ laravel new jettest –jet
$ cd jettest
$ php artisan –version
Laravel Framework 8.15.0

mysql> create database jetstream;
.env

DB_DATABASE=jetstream

$ php artisan migrate
mysql> show tables;
+————————+
| Tables_in_jetstream |
+————————+
| failed_jobs |
| migrations |
| password_resets |
| personal_access_tokens |
| sessions |
| team_user |
| teams |
| users |
+————————+
8 rows in set (0.00 sec)
-> teams, team_user テーブルが出来ている

mysql> describe teams;
+—————+—————–+——+—–+———+—————-+
| Field | Type | Null | Key | Default | Extra |
+—————+—————–+——+—–+———+—————-+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| user_id | bigint unsigned | NO | MUL | NULL | |
| name | varchar(255) | NO | | NULL | |
| personal_team | tinyint(1) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+—————+—————–+——+—–+———+—————-+
6 rows in set (0.00 sec)

// Team版のLivewireをインストール
$ php artisan jetstream:install livewire –teams
$ npm install && npm run dev
$ php artisan serve –host 192.168.33.10 –port 8000

register
L 2つのユーザを作成します。

hpscript
hpscript@gmail.com

yamada
yamada@gmail.com

login後
manage team “Team Settings, Create New Team”が追加されています。

### Team Setting
Team Name: Let’s Go
Add Team Member: メンバーのEmailを追加

### 役割、権限の編集
– “general”の権限を追加します
app/Providers/JetstreamServiceProvider.php

protected function configurePermissions()
    {
        Jetstream::defaultApiTokenPermissions(['read']);

        Jetstream::role('admin', __('Administrator'), [
            'create',
            'read',
            'update',
            'delete',
        ])->description(__('Administrator users can perform any action.'));

        Jetstream::role('editor', __('Editor'), [
            'read',
            'create',
            'update',
        ])->description(__('Editor users have the ability to read, create, and update.'));

        Jetstream::role('general', __('General'), [
            'general',
        ])->description(__('General'));
    }

Roleにgeneralが追加されました。

hpscript@gmail.comでログインして、yamada@gmail.comを”general”のroleで追加します。

mysql> select * from team_user;
+—-+———+———+———+———————+———————+
| id | team_id | user_id | role | created_at | updated_at |
+—-+———+———+———+———————+———————+
| 1 | 1 | 2 | general | 2020-11-18 05:37:18 | 2020-11-18 05:37:18 |
+—-+———+———+———+———————+———————+
1 row in set (0.00 sec)

resources/views/test.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
	<h1>jetstream test</h1>
</body>
</html>

routes/web.php

Route::get('/test', '\App\Http\Controllers\TestController@index');

$ php artisan make:controller TestController

app/Http/Controllers/TestController.php

class TestController extends Controller
{
    //
    public function index()
    {    
        return view('test.index');
    }
}

app/Http/Controllers/TestController.php

public function index(Request $request)
    {    
    	$team = $request->user()->currentTeam;

    	if(auth()->user()->hasTeamPermission($team, 'read')){
    		return view('test');
    	}
        return view('dashboard');
    }

yamadaでログインして、current teamをlet’s goにする。

そして、/testを叩くと、dashboardにリダイレクトされる。

簡単なのかややこしいのかわからんが、roleって概念だから、teamとはちょっと違う気がするんだような。
さて、頑張って設計書作るかー モチベーションを上げないと。。。

[PHP7.4.11] 二次元配列に空白の列を追加してCSVでダウンロード

### CSVの作成
– 文字化けしないよう、stream_filter_prepend($f,’convert.iconv.utf-8/cp932′);の一文を挿入する

$ary = [
	["名前", "年齢", "血液型", "備考"],
	["山田", "12", "O",""],
	["田中", "20", "A",""],
	["吉田", "18", "AB",""],
	["伊藤", "19", "B", "エンジニア"]
]; 

$filename = "test.csv";
$f = fopen($filename, "w");
stream_filter_prepend($f,'convert.iconv.utf-8/cp932');
if($f) {
	foreach($ary as $line){
		fputcsv($f, $line);
	}
}

fclose($f);

### 配列にカラムを追加

$ary = [
	// ["名前", "年齢", "血液型", "備考"],
	["山田", "12", "O",""],
	["田中", "20", "A",""],
	["吉田", "18", "AB",""],
	["伊藤", "19", "B", "エンジニア"]
]; 
$column = ["名前", "年齢", "血液型", "備考"];

array_unshift($ary,$column);

### 配列の途中に空白を追加

$column = ["名前", "年齢", "血液型", "備考"];

array_splice($column, 1, 0, "");

echo "<pre>";
var_dump($column);
echo "</pre>";

### 二次元配列の途中に空白を追加

// DBから抽出
$ary = [
	["山田", "12", "O",""],
	["田中", "20", "A",""],
	["吉田", "18", "AB",""],
	["伊藤", "19", "B", "エンジニア"]
]; 

// 空白追加
foreach($ary as $key => $value){
	 array_splice($ary[$key], 1, 0, "");
}

// カラム追加
$column = ["名前", "無記入", "年齢", "血液型", "備考"];
array_unshift($ary, $column);

echo "<pre>";
var_dump($ary);
echo "</pre>";

これでCSVをエクスポートすると…

### csv.fileのダウンロード

$filename = "test.csv";
header('Content-Type: application/octet-stream');
header('Content-Length: '.filesize($filename));
header('Content-Disposition: attachment; filename=test.csv');

readfile($filename);

作成とダウンロードを一緒にやる場合の方が多いと思うが、予め作成したcsvファイルをダウンロードするだけ、ということも可能。
OK, 次行こう。

[PHP7.4.11] 配列の足し算

$ary = [
	"いちご" => 1,
	"バナナ" => 2
];

foreach($ary as $key => $value){
	if($key == "いちご"){
		$ary["いちご"] = $value + 3;
	}
}
echo "<pre>";
var_dump($ary);
echo "</pre>";


なるほど、これを応用する

$ary = [
	"A商事" => 1200,
	"B商事" => 2000
];


$name ="C商事";
$subtotal = 2000;

foreach($ary as $key => $value){
	if($key == $name){
		$ary[$key] = $value + $subtotal;
	} else {
		$ary[$name] = $subtotal;
	}
}

echo "<pre>";
var_dump($ary);
echo "</pre>";

OK, これをLaravelでやりたい。

[PHP7.4.11] 多次元配列

$ary = [
	"fruits" => ["apple", "banana"],
	"animal" => ["monkey", "cat"]
];

$ary["fruits"][] = "kui";

foreach($ary as $key => $value){
	echo "-" . $key . "<br>";
	foreach($value as $k => $v){
		echo $v . " ";
	}
	echo "<br>";
}

– 二次元配列の中に配列を入れたい

$ary = [
	"A商事" => [
		"注文1" => ["商品1", "500円", "3個", "1500円"],
		"注文2" => ["商品2", "600円", "2個", "1200円"]
	],
	"B商事" => [
		"注文3" => ["商品3", "700円", "1個", "700円"]
	]
];

$ary["B商事"]["注文4"] = ["商品4", "200円", "10個", "2000円"];

echo "<pre>";
var_dump($ary);
echo "</pre>";

これを呼び出したい

foreach($ary as $key => $value){
	echo "-". $key . "<br>";
	foreach($value as $k => $v){
		echo $k. ":";
		foreach($v as $n => $data){
			echo $data . " ";
		}
	}
	echo "<br>";
}

OK、 これに$keyごとの小計を加えたい