ver.2 では、まだアプリ複製機能が実装されていません。(※)
今回は、 アプリ複製機能(アプリとDBのみ)を作ってみました。
アプリを複製して、どんどんアプリを作りたいと思っている方は、ぜひ参考にしてください。
※ 今後、実装される可能性があります。
前提
ver.2 のページ機能にプログラムを設置し、画面上から複製元のアプリIDを入力できるようにしています。
簡易バージョンで作成しているため、参照フィールドが複数DBにまたがって使用されている場合、
またがっている参照フィールドを削除し、DB作成を行っています。
複数DBにまたがる参照フィールドがあるパターンや大量のDBがあるアプリなどの場合は、調整が必要です。
PHP / API
PHPの処理を書きます。
[API_KEY]は、各環境に合わせた値の設定をお願いします。
変更が必要な箇所は、API_KEY の部分のみです。
今回はアプリ作成を行うため、APIキー は、アプリ作成権限を持ったユーザに紐づくAPIキーが必要となります。
参考:ユーザのAPIキー
<?php define("API_URL", "https://api.spiral-platform.com/v1/"); define("API_KEY", ""); $commonBase = CommonBase::getInstance(); // appIDがPOSTされた場合に、処理を実行 if($SPIRAL->getParam("appID")){ // 処理が実行されたため、結果部分が表示するよう Thymeleaf にフラグを渡す $SPIRAL->setTHValue("postAction", true); // エラーフラグを一旦立ない状態で、Thymeleaf に値を渡す $SPIRAL->setTHValue("error", false); // アプリを取得 $addData = $commonBase->apiCurlAction("GET","apps/".$SPIRAL->getParam("appID")); // DB名を値が送られてきた値もしくは自動で命名 if($SPIRAL->getParam("appName")){ $newAppData["name"] = $SPIRAL->getParam("appName"); }else{ $newAppData["name"] = "c_".$addData["name"]; } if($SPIRAL->getParam("displayName")){ $newAppData["displayName"] = $SPIRAL->getParam("displayName"); }else{ $newAppData["displayName"] = "c_".$addData["displayName"]; } $newAppData["description"] = "".$addData["description"]; // アプリの複製 $newApp = $commonBase->apiCurlAction("POST","apps/",$newAppData); if(isset($newApp["status"]) && $newApp["status"] != 200){ // アプリの複製に失敗 $SPIRAL->setTHValue("error", true); $SPIRAL->setTHValue("errorLocation", "APP Create"); $SPIRAL->setTHValue("apiMessage", print_r($newApp, true)); }else{ // アプリ内のDB情報取得 $addData = $commonBase->apiCurlAction("GET","apps/".$SPIRAL->getParam("appID")."/dbs?where=@limit=200"); if(isset($addData["status"]) && $addData["status"] != 200){ // DB情報取得に失敗 $SPIRAL->setTHValue("error", true); $SPIRAL->setTHValue("errorLocation", "APP Create"); $SPIRAL->setTHValue("apiMessage", print_r($newApp, true)); }else{ $error = array(); $reference = array(); // DBを作成 for($i=0;$i<$addData["totalCount"];$i++){ $skip = false; $oldDbId = $addData["items"][$i]["id"]; // 取得したDB内の不要フィールド情報を削除 foreach($addData["items"][$i]["fields"] as $key => $count){ unset($addData["items"][$i]["fields"][$key]["id"]); unset($addData["items"][$i]["fields"][$key]["fieldOrder"]); unset($addData["items"][$i]["fields"][$key]["size"]); // 参照フィールドがある場合は、スキップ if($addData["items"][$i]["fields"][$key]["type"] == "reference"){ $skip = true; } } // 取得したDB内の不要情報を削除 unset($addData["items"][$i]["id"]); unset($addData["items"][$i]["revision"]); unset($addData["items"][$i]["createdBy"]); unset($addData["items"][$i]["updatedBy"]); unset($addData["items"][$i]["createdAt"]); unset($addData["items"][$i]["updatedAt"]); if($skip){ $reference[] = $i; }else{ // DBを作成 $newDB = $commonBase->apiCurlAction("POST","apps/".$newApp['id']."/dbs",$addData["items"][$i]); if(isset($newDB["id"])){ // DB生成成功した場合、IDをリストに追加 $dbList[$oldDbId] = $newDB["id"]; }else{ // 失敗した場合、エラーメッセージを配列に追加 $error[$addData["items"][$i]["name"]] = $newDB; } } // 秒間10リクエストを超えないよう、5つDB作成したら、1秒ストップ if($i % 5 == 0){ sleep(1); } } // 参照フィールドがあり、スキップされたDBがある場合 if(!empty($reference)){ $referenceDelMessage = array(); $count = 1; // スキップがあるDBを繰り返し処理 foreach($reference as $key => $val){ foreach($addData["items"][$val]["fields"] as $key2 => $val2){ // 参照フィールドのカラム と DB生成成功のリスト内に参照先のDBがあるかチェック if($addData["items"][$val]["fields"][$key2]["type"] == "reference" && isset($dbList[$addData["items"][$val]["fields"][$key2]["referenceDb"]["id"]])){ // 参照フィールドのカラムの場合、作成したDBと紐づけ $addData["items"][$val]["fields"][$key2]["referenceDbId"] = $dbList[$addData["items"][$val]["fields"][$key2]["referenceDb"]["id"]]; $addData["items"][$val]["fields"][$key2]["referenceLabelFieldId"] = $addData["items"][$val]["fields"][$key2]["referenceLabelField"]["id"]; unset($addData["items"][$val]["fields"][$key2]["referenceDb"]); unset($addData["items"][$val]["fields"][$key2]["referenceLabelField"]); }else if($addData["items"][$val]["fields"][$key2]["type"] == "reference"){ // DB生成成功のリスト内に参照先のDBがない場合は、参照フィールドを削除 unset($addData["items"][$val]["fields"][$key2]); $referenceDel = true; } } // DBを作成 $referenceDB = $commonBase->apiCurlAction("POST","apps/".$newApp['id']."/dbs",$addData["items"][$val]); if(!isset($referenceDB['id'])){ // DB作成に失敗した場合、エラーメッセージを配列に追加 $error[$addData["items"][$val]["name"]] = $referenceDB; }elseif(!empty($referenceDel)){ // 参照フィールドを削除したDBのリスト生成 $referenceDelMessage[$addData["items"][$val]["name"]] = $referenceDB["displayName"]; } // 秒間10リクエストを超えないよう、5つDB作成したら、1秒ストップ if($count % 5 == 0){ sleep(1); } $count++; } } if(!empty($error)){ // エラーが発生している場合、メッセージをThymeleafにセット $SPIRAL->setTHValue("error", true); $SPIRAL->setTHValue("errorLocation", "DB create"); $SPIRAL->setTHValue("apiMessage", print_r($error, true)); } if(!empty($referenceDel)){ // 参照フィールドを削除したDBが存在する場合、リストをThymeleafにセット $SPIRAL->setTHValue("referenceDel", true); $SPIRAL->setTHValue("referenceDelMessage", "参照フィールドが削除されているDB:". print_r($referenceDelMessage, true)); } } } }else{ $SPIRAL->setTHValue("postAction", false); } //------------------------------ // 共通用モジュール //------------------------------ class CommonBase { /** * シングルトンインスタンス * @var UserManager */ protected static $singleton; public function __construct() { if (self::$singleton) { throw new Exception('must be singleton'); } self::$singleton = $this; } /** * シングルトンインスタンスを返す * @return UserManager */ public static function getInstance() { if (!self::$singleton) { return new CommonBase(); } else { return self::$singleton; } } /** * V2用 API送信ロジック * @return Result */ function apiCurlAction($method, $addUrlPass, $data = null) { $header = array( "Authorization:Bearer " . API_KEY, "Content-Type:application/json", "X-Spiral-Api-Version: 1.1", ); // curl $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_URL, API_URL . $addUrlPass); curl_setopt($curl, CURLOPT_HTTPHEADER, $header); if ($method == "POST") { curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); } if ($method == "PATCH") { curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); } if ($method == "DELETE") { curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); } if ($method == "GET") { curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); } $response = curl_exec($curl); if (curl_errno($curl)) echo curl_error($curl); curl_close($curl); return json_decode($response, true); } }
HTML(Thymeleaf)
アプリIDを入力するための画面 と アプリ複製の処理結果を表示させる処理を記載
HTML(Thymeleaf)<div class="box_con"> <h1>ver.2 アプリ複製ツール</h1> <form method="post" action="#"> <table class="formTable"> <tr> <th>複製元アプリID<span>必須</span></th> <td><input size="30" type="text" name="appID" pattern="^[0-9A-Z_a-z]+$" required="required" value="" /></td> </tr> <tr> <th>表示名</th> <td> <input size="30" type="text" name="displayName" maxlength="64" placeholder="未入力の場合、コピー元の表示名の接頭に「c_」がついた表示名になります。" /><br /> <span class="contents-note">* 既存アプリの表示名と重複不可</span><br /> </td> </tr> <tr> <th>識別名</th> <td> <input size="30" type="text" name="appName" pattern="^[0-9A-Z_a-z]+$" maxlength="64" placeholder="未入力の場合、コピー元の識別名の接頭に「c_」がついた識別名になります。" /><br /> <span class="contents-note">* アルファベット大文字・小文字、数字、アンダースコアのみ使用可能</span><br /> <span class="contents-note">* 最初の文字はアルファベット</span><br /> <span class="contents-note">* 既存アプリの識別名と重複不可</span><br /> </td> </tr> </table><br /> <p class="btn"> <span><input type="submit" value=" 確認 " /></span> </p> </form> <div th:if="${!cp.result.isSuccess}"> <p th:text="${cp.result.errorMessage}">error message</p> </div> <div class="con_pri" style="margin-top: 20px;"> <div th:if="${cp.result.value['postAction'] == true}" class="box_pri"> <div class="box_tori"> <h4>実行結果</h4> <!--/* エラー時 */--> <p th:if="${cp.result.value['error']}" th:text="${cp.result.value['errorLocation']}"></p> <p th:if="${cp.result.value['error']}" th:text="${cp.result.value['apiMessage']}"></p> <!--/* 成功時 */--> <p th:if="${!cp.result.value['error']}">success!</p> <!--/* 共有フィールド削除がある場合 */--> <p th:if="${cp.result.value['referenceDel']}" th:text="${cp.result.value['referenceDelMessage']}"></p> </div> </div> </div> </div>
CSS
一応、見やすいようにCSSも追加
CSS/*リセットcss↓*/ html { overflow-y: scroll; } body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td { margin: 0; padding: 0; } address,caption,cite,code,dfn,em,strong,th,var { font-style: normal; } table { border-collapse: collapse; border-spacing: 0; } caption,th { text-align: left; color: #217599; } q:before,q:after { content: ''; } object,embed { vertical-align: top; } hr,legend { display: none; } h1,h2,h3,h4,h5,h6 { font-size: 100%; } img,abbr,acronym,fieldset { border: 0; } li { list-style-type: none; } sup { vertical-align: super; font-size: 0.5em; } img { vertical-align: top; } i { font-style: normal; } /*----リセットcss*----/ /*デザインcss↓*/ .box_con { max-width: 900px; margin: 0 auto; } @media only screen and (max-width: 768px) { .box_con { width: 95%; } } .box_con form { width: 100%; } .box_con form table { width: 100%; } .box_con form table tr { position: relative; } .box_con form table tr:after { content: ""; position: absolute; width: 100%; left: 0; bottom: 0; height: 1px; border-bottom: dotted #cdcdcd 1px; } .box_con form table tr th { width: 30%; font-weight: normal; padding: 1em .5em; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } @media only screen and (max-width: 768px) { .box_con form table tr th { text-align: center; width: 100%; display: block; background: #97ae88; padding: .8em .2em; color: #fff; } } .box_con form table tr th span { background: #cd6f55; padding: 0 .3em; color: #fff; margin-left: .5em; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } .box_con form table tr td { padding: 1em .5em; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } @media only screen and (max-width: 768px) { .box_con form table tr td { padding: 1.5em .5em; display: block; width: 100%; } } .box_con form table tr .box_br { display: block; } .box_con form table tr select { border: 1px solid #97ae88; } .box_con form table tr label input { cursor: pointer; display: none; vertical-align: middle; } .box_con form table tr .radio02-input + label { padding-left: 23px; margin-right: 20px; position: relative; } .box_con form table tr .radio02-input + label:before { content: ""; display: block; position: absolute; top: 50%; left: 0; width: 16px; height: 16px; border: 1px solid #999; border-radius: 50%; -moz-transform: translateY(-50%); -ms-transform: translateY(-50%); -webkit-transform: translateY(-50%); transform: translateY(-50%); } .box_con form table tr .radio02-input:checked + label:after { content: ""; display: block; position: absolute; top: 50%; left: 3px; width: 12px; height: 12px; background: #97ae88; border-radius: 50%; -moz-transform: translateY(-50%); -ms-transform: translateY(-50%); -webkit-transform: translateY(-50%); transform: translateY(-50%); } .box_con form table tr select, .box_con form table tr input, .box_con form table tr textarea { width: 100%; height: 3em; padding: .5em; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } .box_con form table tr textarea { height: 10em; } /*プライバシーのデザインcss↓*/ .con_pri { max-width: 700px; margin: 0 auto; } @media only screen and (max-width: 768px) { .con_pri { width: 95%; } } .con_pri .box_pri { height: 300px; overflow-y: scroll; border: 1px solid #cdcdcd; background: #f7f7f7; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; margin-top: 20px; padding: 20px 55px; } @media only screen and (max-width: 768px) { .con_pri .box_pri { margin-top: 4%; padding: 3%; } } @media only screen and (min-width: 769px) and (max-width: 1024px) { .con_pri .box_pri { padding: 4%; } } .con_pri .box_pri .box_tori { text-align: left; margin-top: 40px; } @media only screen and (max-width: 768px) { .con_pri .box_pri .box_tori { margin-top: 4%; } } .con_pri .box_pri .box_tori h4 { font-weight: normal; margin-bottom: 30px; font-size: 150%; } @media only screen and (max-width: 768px) { .con_pri .box_pri .box_tori h4 { margin-bottom: 4%; } } .con_pri .box_pri .box_tori .txt { padding: 0 20px; } @media only screen and (max-width: 768px) { .con_pri .box_pri .box_tori .txt { padding: 0; } } .con_pri .box_pri .box_num { margin-top: 30px; } @media only screen and (max-width: 768px) { .con_pri .box_pri .box_num { margin-top: 5%; } } .con_pri .box_pri .box_num h4 { font-weight: normal; font-size: 113%; } .con_pri .box_pri .box_num .txt { padding: 10px 0 0 20px; } @media only screen and (max-width: 768px) { .con_pri .box_pri .box_num .txt { padding: 3% 0 0 3%; } } .box_check { text-align: center; margin: 1em auto; } .box_check label { display: inline-block; } .box_check label span { margin-left: .3em; } .btn { text-align: center; } .btn input { display: inline-block; background: #eee; padding: .5em 4em; color: #000; text-decoration: none; cursor: pointer; border: none; -moz-transition: all 0.4s; -o-transition: all 0.4s; -webkit-transition: all 0.4s; transition: all 0.4s; } .btn input:hover { filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70); opacity: 0.7; } h1 { border-bottom: solid 3px #69d3a6; position: relative; font-size: 2.75em; color: #217599; } h1:after { position: absolute; content: " "; display: block; border-bottom: solid 3px #1aa3bf; bottom: -3px; width: 20%; } .contents-note{ font-size: 0.9em; color: #777; }
無理やり作ってしまっているところもあるので、アプリ複製はしっかり確認してください。
不具合やほかのやり方がある等あれば、下記の「コンテンツに関しての要望はこちら」からご連絡ください。