作成日:2021/04/12 更新日:2022/08/05

#6 jQueryで要素をドラッグ移動させる

jQueryの設定


<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script>
$(function(){
  $('.drag').draggable({       /* class="drag"が指定されている要素をdraggableに */
    containment:'#drag-area',  /* ドラッグできる範囲 */
    cursor:'move',             /* ドラッグ時のカーソル形状 */
    opacity:0.6,               /* ドラッグ中の透明度 */
    scroll:true,               /* ウィンドウ内をスクロールしたい */
    zIndex:10,                 /* ドラッグ中の重ね順を一番上に */
  });
});
</script>
動きました。
しかし、データベースからcssのabsoluteプロパティ値「top:〜;」「left:〜;」をの〜部分を読み込んでいるので、 再読込すれば当然位置は元に戻ってしまいます。
この位置が再読込しても移動しないようにするには、移動してマウスを離す「イベント」の際に、xとy座標の値(数値)をデータベースに登録するように変更しましょう。

要素の(x,y)の値を取得

座標の値を取得するには、先程のjsを拡張して「マウスを離した」というイベントに処理を書きたいと思います。

<script>
$(function(){
  $('.drag').draggable({
    containment:'#drag-area',
    cursor:'move',
    opacity:0.6,
    scroll:true,
    zIndex:10,
    /* ==========STOP処理====================================== */
    stop:function(event, ui){               /* ドラッグ終了時に起動 */
      let myNum  = $(this).data('num');     /* data-num="値" の値を取得 */
      let myLeft = (ui.offset.left - $('#drag-area').offset().left);    /* leftの座標 = 要素の座標 - #drag-areaの座標 */
      let myTop  = (ui.offset.top  - $('#drag-area').offset().top);     /* topの座標  = 要素の座標 - #drag-areaの座標 */
        console.log("左: " + myLeft);     /* x座標がとれている */
        console.log("上: " + myTop);      /* y座標がとれている */
    }
    /* ==========/STOP処理====================================== */
  });
});
</script>
htmlで要素には「data-num」という名前をつけてそこにID値を入れています。(data-id="1")
jQueryではdata属性値を取得できます。 .data('num') というように、 カッコの中に-(ハイフン)以降の使用した文字列を記述します。

こうすることで、1や2などの値を取得できるようになりました。

.data('num')
本来、$(div)や、$('area')といったセレクタ・class名で指定したいところですが、該当セレクタの全てを取得してしまいます。 一斉に操作する場合は有効だが、動かした要素の値だけ欲しいので、その場合は $(this) を使います。

$(this).data('num');
let myNum で、取得したIDを「myNum」という変数に格納してます。

let myNum = $(this).data('num');
ウィンドウの幅は環境によって様々ですので、bodyから見た #drag-area(ドラッグできる範囲)の絶対位置(左上の座標)を取得します。

$('#drag-area').offset().left /* #drag-area 左の値 */
$('#drag-area').offset().top  /* #drag-area 上の値 */

つぎに#drag-areaを起点(0, 0)にした場合の .drag の座標を取得して表示させたいとします。

bodyからの.drag の絶対位置(250, 152) から bodyからの#drag-areaの絶対位置(223, 95)を 引くと
250-223, 152-95で、#drag-areaからみた.dragの座標(27, 57)の値が取れます。

let myLeft = (ui.offset.left - $('#drag-area').offset().left);
let myTop  = (ui.offset.top  - $('#drag-area').offset().top);
ui.offset.left は bodyから見た.dragの左側の絶対位置
$('#drag-area').offset().left は bodyから見た#drag-areaの左側の絶対位置

console.log();で値を確認


console.log(myLeft);
マウスを離した瞬間にデータが取得できているのがわかります。
※1pxのずれは親子borderの誤差分です。

#7 データをAjaxでPHPに送信しデータベースに登録

AjaxでPOSTする

(x,y)座標は取得出来ました。
今度はAjaxを使って、PHPに変数を渡します。先程のjsを拡張します。

<script>
$(function(){
  $('.drag').draggable({
    containment:'#drag-area',
    cursor:'move',
    opacity:0.6,
    scroll:true,
    zIndex:10,
    /* ==========STOP処理====================================== */
    stop:function(event, ui){               /* ドラッグ終了時に起動 */
      let myNum  = $(this).data('num');     /* data-num="値" の値を取得 */
      let myLeft = (ui.offset.left - $('#drag-area').offset().left);    /* leftの座標 = 要素の座標 - #drag-areaの座標 */
      let myTop  = (ui.offset.top  - $('#drag-area').offset().top);     /* topの座標  = 要素の座標 - #drag-areaの座標 */
      /* ==========AJAX通信================= */
      $.ajax({
        type:'POST',      /* typeパラメーター:POSTかGETか */
        url :'http://localhost:8001/',  /* urlパラメーター:飛ばす先のファイル名(今回は自分に戻ってくる) */
        data: {           /* dataパラメーター:データをphpに渡す data:{ foo:'引数', bar:'引数' } でPHP側は $_POST['foo'] で'引数'の値を受け取る */
          id  :myNum,     /* key:valueの関係、つまりidのというkeyとそれに入れる値valueはmyNumとなる */
          left:myLeft,
          top :myTop
        }
      }).done(function(){   /* ajaxの通信に成功した場合の処理 */
         console.log('成功');
      }).fail(function(XMLHttpRequest, textStatus, errorThrown){  /* ajaxの通信に失敗した場合エラー表示 */
         console.log(XMLHttpRequest.status);
         console.log(textStatus);
         console.log(errorThrown);
      });
      /* ==========/AJAX通信================= */
        console.log("左: " + myLeft);    /* x座標がとれている */
        console.log("上: " + myTop);      /* y座標がとれている */
    }
    /* ==========/STOP処理====================================== */
  });
});
</script>
$.ajaxメソッドを使い通信します。成功時にはdoneメソッド、失敗時はfailメソッドを処理しますので、consoleして内容を確認してましょう。


data: {
  id  :myNum,
  left:myLeft,
  top :myTop
}
data:パラメーターでは複数の配列変数を同時にPOST出来ます。
jsで作った変数「myLeft」のデータに「left」という添字をつけてPOSTするという意味です。 POSTされたデータは $_POST['left'] となり、中身にはx座標の数値データが入っているはずです。

POSTされたデータをデータベースに登録

if文の引数である(!empty($_POST['left']))の !empty で、データがあるかどうかを判別しています。$_POST['left']というデータがajaxによりpostされ存在しているなら、それ以下の処理に進みます。

if(!empty($_POST['left'])){
  try{
    $sql  = 'UPDATE `sortable` SET `left_x` = :LEFT, `top_y` = :TOP WHERE `id` = :NUMBER';
    $stmt = $dbh->prepare($sql);
    $stmt->bindValue(':LEFT'  , $_POST['left'], PDO::PARAM_INT);
    $stmt->bindValue(':TOP'   , $_POST['top'],  PDO::PARAM_INT);
    $stmt->bindValue(':NUMBER', $_POST['id'],   PDO::PARAM_INT);
    $stmt->execute();
  } catch (PDOException $e) {
    echo $e->getMessage();
  }
}
prepareメソッド(関数)はqueryメソッド(関数)のようなもので、引数に指定したSQL文をデータベースに対して発行してくれます。 queryで発行するSQL文と違い「:LEFT」のようなパラメータを使って 後で 実際の値を入れてSQL文を完成、発行させます。
ですので、値が毎回異なるSQL文の発行はprepareメソッドを使います。

$stmt = $dbh->prepare($sql);


$sql  = '
  UPDATE
    `sortable`
  SET
    `left_x` = :LEFT,
    `top_y` = :TOP
  WHERE
    `id` = :NUMBER
';
解説↓

$sql ='
  UPDATE
    `sortable`         /* `sortable`というテーブルを上書き(UPDATE)します */
  SET
    `left_x` = :LEFT,  /* DBのカラム`left_x`に「:LEFT」というパラメータの値をUPDATEする宣言 */
    `top_y`  = :TOP    /* DBのカラム`top_y`に「:TOP」というパラメータの値をUPDATEする宣言 */
  WHERE
    `id` = :NUMBER     /* WHEREで条件式にマッチするレコードを選択します。つまりIDが同じものを探し出します */
';

/* $stmtはPDOStatementオブジェクトになる */
$stmt = $dbh->prepare($sql);

/* bindValue 実際の値をパラメータにバインドする */
$stmt->bindValue(':LEFT', $_POST['left'], PDO::PARAM_INT);
               /* :パラメータ名,   実際の値,  データタイプ */

/* ここで、実際の値をSQL文に挿入して発行させます */
$stmt->execute();
変数名にパラメーターを紐付け(バインド)する処理です。
PDO::PARAM_INTは、データの型を指定することで、これから入力されるデータが数字であることを宣言します。

わざわざ、データをバインドする意味

プレースホルダという考え方があります。紐づけ用のラベル(パラメーター)を用意し準備だけしておいて、後で実際の値を入れ(バインド)、SQL文を発行する(execute)という処理を行っていますが、 これは、SQLインジェクションという攻撃を避けるため(と高速化のため)です。
プレースホルダは、SQL文をエスケープする処理も行っています。このおかげで本来入ってはいけない文章が入り込むのを防いでいるわけです。

$sql  = '
  UPDATE
    `sortable`
  SET
    `left_x` = :LEFT,
    `top_y`  = :TOP
  WHERE
    `id` = :NUMBER
';
上のSQL文を以下のように書いてはいけない!

$sql  = '
  UPDATE
    `sortable`
  SET
    `left_x` = $_POST['left'], ←変なデータが入力されてもそのままアップデートされてしまう
    `top_y`  = $_POST['top']
  WHERE
    `id` = $_POST['id']
';
SQL文をエスケープすることでハッキングなどのSQLインジェクション攻撃を防げます。

要素の位置を記録しているので、リロードしても移動させた位置で表示されます。

完成したindex.php


<?php
error_reporting(-1);

/* データベース設定 */
define('DB_DNS', 'mysql:host=localhost; dbname=cri_sortable; charset=utf8');
define('DB_USER', 'root');
define('DB_PASSWORD', 'root');

/* データベースへ接続 */
try {
  $dbh = new PDO(DB_DNS, DB_USER, DB_PASSWORD);
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $e){
    echo $e->getMessage();
    exit;
}

/* 新規氏名をデータベースへ登録 */
if(!empty($_POST['inputName'])){
  try{
    $sql  = 'INSERT INTO sortable(name) VALUES(:ONAMAE)';
    $stmt = $dbh->prepare($sql);

    $stmt->bindValue(':ONAMAE', $_POST['inputName'], PDO::PARAM_STR);
    $stmt->execute();

    header('location: http://localhost:8001/');
    exit();
  } catch (PDOException $e) {
      echo 'データベースにアクセスできません!'.$e->getMessage();
  }
}
/* 移動したようその座標をデータベースへ登録 */
if(!empty($_POST['left'])){
  try{
    $sql  = 'UPDATE `sortable` SET `left_x` = :LEFT, `top_y` = :TOP WHERE `id` = :NUMBER';
    $stmt = $dbh->prepare($sql);
    $stmt->bindValue(':LEFT'  , $_POST['left'], PDO::PARAM_INT);
    $stmt->bindValue(':TOP'   , $_POST['top'],  PDO::PARAM_INT);
    $stmt->bindValue(':NUMBER', $_POST['id'],   PDO::PARAM_INT);
    $stmt->execute();
  } catch (PDOException $e) {
    echo $e->getMessage();
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>8001-cri-sortable</title>
  <link href="css/style.css" rel="stylesheet">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script>
$(function(){
  $('.drag').draggable({
    containment:'#drag-area',
    cursor:'move',
    opacity:0.6,
    scroll:true,
    zIndex:10,
    /* ==========STOP処理====================================== */
    stop:function(event, ui){
      let myNum  = $(this).data('num');
      let myLeft = (ui.offset.left - $('#drag-area').offset().left);
      let myTop  = (ui.offset.top  - $('#drag-area').offset().top);
      /* ==========AJAX通信================= */
      $.ajax({
        type:'POST',
        url :'http://localhost:8001/',
        data: {
          id  :myNum,
          left:myLeft,
          top :myTop
        }
      }).done(function(){
         console.log('成功');
      }).fail(function(XMLHttpRequest, textStatus, errorThrown){
         console.log(XMLHttpRequest.status);
         console.log(textStatus);
         console.log(errorThrown);
      });
      /* ==========/AJAX通信================= */
        console.log("左: " + myLeft);
        console.log("上: " + myTop);
    }
    /* ==========/STOP処理====================================== */
  });
});
</script>
</head>
<body>
<div id="wrapper">

<div id="input_form">
  <form action="index.php" method="POST">
    <input type="text" name="inputName">
    <input type="submit" value="登録">
  </form>
</div>

<div id="drag-area">
<?php
$sql = 'SELECT * FROM sortable';
$stmt = $dbh->query($sql);
foreach ($stmt as $result){
  echo '  <div class="drag" data-num="'.$result['id'].'" style="left:'.$result['left_x'].'px; top:'.$result['top_y'].'px;">'.PHP_EOL;
  echo '    <p><span class="name">'.$result['id'].' '.$result['name'].'</span></p>'.PHP_EOL;
  echo '  </div>'.PHP_EOL;
}
?>
</div>

</div>
</body>
</html>
▼データの流れの図解

#8 PHPで簡単にできるデバッグ方法

PHPでエラーが出ても直せない場合など、jsのconsoleのように今どんなデータが変数に入っているのか確認したいときがあります。 そういうときはデータの内容をテキストデータにして出力するという機能がありますので、利用してみましょう。
例えば座標を登録する際に、jsのconsoleログでは数値を確認できたのに、データベースに登録されないというケースがあったとします。

file_put_contents('log.txt', $_POST['top']);
$_POST['top']というデータを、log.txtという名前で保存する。という意味になります。 実行されるとフォルダにlog.txtが出力されるので開いてみてみましょう。


if(!empty($_POST['left'])){  /* 要素が移動したら、Ajaxからのデータを受信してこの中の処理が走る */
  file_put_contents('log.txt', 'ここまできたかな?');

  try{
    $sql  = 'UPDATE `sortable` SET `left_x` = :LEFT, `top_y` = :TOP WHERE `id` = :NUMBER';
    $stmt = $dbh->prepare($sql);
    $stmt->bindValue(':LEFT'  , $_POST['left'], PDO::PARAM_INT);
    $stmt->bindValue(':TOP'   , $_POST['top'],  PDO::PARAM_INT);
    $stmt->bindValue(':NUMBER', $_POST['id'],   PDO::PARAM_INT);

    file_put_contents('log2.txt', print_r($stmt, true));

    $stmt->execute();
  } catch (PDOException $e) {
    echo $e->getMessage();
  }
}
try catchの前や、処理中のところに記述しておきます。
これで、PHPのどの状態のときにどんなデータが入っているのか確認することができます。

#9 作ったプログラムを拡張してみる

先程作ったプログラムに、性別を判別して男性は青い線、女性は赤い線をつけて表示させましょう。
▼手順
  1. 新たに性別の名称を保存するテーブルgendersを作り(複数形のsをつけてください)、
    カラムid [tinyint(2)]gender [varchar(11)](sは なし)を作成し、
    idの1に男性、idの2を女性とする。
    構造

    表示
  2. 既存のテーブル「sortable」に性別用カラムである「gender_id」を作り、phpMyAdminから男性を「1」女性は「2」を入力。
    構造

    表示
  3. 要素をforeachでechoする際に、dragクラスと同様に、gender1、gender2というクラス名(*1)を性別データごとに追記します。
    div class="drag gender1" や div class="drag gender2" のようにgender_idの数字をタグ内に表示させるようにする。
    答え
    
    echo '  <div class="drag gender'.$result['gender_id'].'" data-num=...;
    
  4. cssファイルで、セレクタ.gender1 .gender2に、borderプロパティで青色(#00F)と赤色(#F00)を設定する。
  5. 新規登録の際に男性女性の情報を、DBのsortableテーブル`gender_id`カラムに登録したいため、htmlのformタグ内にinputタグで男女を選択できるラジオボタンを設置する。 その際、inputタグで表示するラジオボタンの文字列「男性、女性」はDBのテーブル`gender`から取得し、予めどちらかにcheckedを設定しておく。
    
    <input type="text" name="inputName" placeholder="新メンバー名を入力">
    の下辺りに以下のロジックを作る。
    
    ①.drag-area内に要素を出力したように、SQL文を発行しDBの中身を取ってきて、
     変数に格納する。
    
    ②変数をループで回して、全データを出力
     inputタグを出力しながら、各要素ごとのデータ(男性か女性)を表示させる
     その際、あらかじめどちらかにcheckedを設定(男性でも女性でも可)したいので、
     データが1(もしくは2)の場合はcheckedを入れる、そうでない場合はなにもしないという処理を、
     三項演算子を使って変数に格納し、input文で使用する。
    
    答えを表示する/非表示にする
    
    <input type="text" name="inputName" placeholder="新メンバー名を入力">
    <?php
    $sql = 'SELECT * FROM genders';
    $stmt = $dbh->query($sql);
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    foreach ($result as $val) {
      $checked = ($val['id'] == 1) ? ' checked="checked"' : ''; /* 男性にチェックを入れる */
      echo '  <input type="radio" name="inputGender" value="'.$val['id'].'"' . $checked . '>'.$val['gender'].PHP_EOL;
    }
    ?>
    <input type="submit" value="登録">
    
  6. 新規登録のSQL文「INSERT INTO sortable(カラムA) VALUES(:パラメータA)」に、選択された性別も登録できるように追記する。
    ヒント
    formの中にあるすべてのname属性値が連想配列$_POST、でPOSTされます。 #7で出てきたUPDATE文は複数のデータ(x座標、y座標)を登録できていますので、それを参考にし、複数のカラムを登録しましょう。 わからない場合はgoogleでMySQL SQL文 INSERT 複数などで調べましょう。
    答えを表示する/非表示にする
    
    <?php
    if(!empty($_POST['inputName'])){
      try{
        $sql  = 'INSERT INTO sortable(name, gender_id) VALUES(:ONAMAE, :GENDER)';
        $stmt = $dbh->prepare($sql);
        $stmt->bindValue(':ONAMAE', $_POST['inputName'],   PDO::PARAM_STR);
        $stmt->bindValue(':GENDER', $_POST['inputGender'], PDO::PARAM_INT);
        $stmt->execute();
      } catch (PDOException $e) {
        echo $e->getMessage();
      }
    }
    ?>
    

※アプリ化するのであれば性別を間違えた場合の編集機能もつけるべきですが、ココでは省きます。
ここまでの答えを表示する

<?php
error_reporting(-1);

/* データベース設定 */
define('DB_DNS', 'mysql:host=localhost; dbname=cri_sortable; charset=utf8');
define('DB_USER', 'root');
define('DB_PASSWORD', 'root');

/* データベースへ接続 */
try {
  $dbh = new PDO(DB_DNS, DB_USER, DB_PASSWORD);
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $e){
    echo $e->getMessage();
    exit;
}

/* 新規氏名+性別をデータベースへ登録 */
if(!empty($_POST['inputName'])){
  try{
    $sql  = 'INSERT INTO sortable(name, gender_id) VALUES(:ONAMAE, :GENDER)';
    $stmt = $dbh->prepare($sql);
    $stmt->bindValue(':ONAMAE', $_POST['inputName'],   PDO::PARAM_STR);
    $stmt->bindValue(':GENDER', $_POST['inputGender'], PDO::PARAM_INT);
    $stmt->execute();
  } catch (PDOException $e) {
    echo $e->getMessage();
  }
}

/* 移動した要素の座標をデータベースへ登録 */
if(!empty($_POST['left'])){
  try{
    $sql  = 'UPDATE `sortable` SET `left_x` = :LEFT, `top_y` = :TOP WHERE `id` = :NUMBER';
    $stmt = $dbh->prepare($sql);
    $stmt->bindValue(':LEFT'  , $_POST['left'], PDO::PARAM_INT);
    $stmt->bindValue(':TOP'   , $_POST['top'],  PDO::PARAM_INT);
    $stmt->bindValue(':NUMBER', $_POST['id'],   PDO::PARAM_INT);
    $stmt->execute();
  } catch (PDOException $e) {
    echo $e->getMessage();
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>8001-cri-sortable</title>
  <link href="css/style.css" rel="stylesheet">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script>
$(function(){
  $('.drag').draggable({
    containment:'#drag-area',
    cursor:'move',
    opacity:0.6,
    scroll:true,
    zIndex:10,
    /* ==========STOP処理====================================== */
    stop:function(event, ui){
      let myNum  = $(this).data('num');
      let myLeft = (ui.offset.left - $('#drag-area').offset().left);
      let myTop  = (ui.offset.top  - $('#drag-area').offset().top);
      /* ==========AJAX通信================= */
      $.ajax({
        type:'POST',
        url :'http://localhost:8001/',
        data: {
          id  :myNum,
          left:myLeft,
          top :myTop
        }
      }).done(function(){
         console.log('成功');
      }).fail(function(XMLHttpRequest, textStatus, errorThrown){
         console.log(XMLHttpRequest.status);
         console.log(textStatus);
         console.log(errorThrown);
      });
      /* ==========/AJAX通信================= */
        console.log("左: " + myLeft);
        console.log("上: " + myTop);
    }
    /* ==========/STOP処理====================================== */
  });
});
</script>
</head>
<body>
<div id="wrapper">

<div id="input_form">
  <form action="index.php" method="POST">
    <input type="text" name="inputName" placeholder="新メンバー名を入力">
    <?php
      $sql    = 'SELECT * FROM genders';
      $stmt   = $dbh->query($sql);
      $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
      foreach ($result as $val) {
        $checked = ($val['id'] == 1) ? ' checked="checked"' : '';
        echo '  <input type="radio" name="inputGender" value="'.$val['id'].'"' . $checked . '>'.$val['gender'].PHP_EOL;
      }
    ?>
    <input type="submit" value="登録">
  </form>
</div>

<div id="drag-area">
<?php
$sql  = 'SELECT * FROM sortable';
$stmt = $dbh->query($sql)->fetchAll(PDO::FETCH_ASSOC);
foreach ($stmt as $result){
  echo '  <div class="drag gender'.$result['gender_id'].'" data-num="'.$result['id'].'" style="left:'.$result['left_x'].'px; top:'.$result['top_y'].'px;">'.PHP_EOL;
  echo '    <p><span class="name">'.$result['id'].' '.$result['name'].'</span></p>'.PHP_EOL;
  echo '  </div>'.PHP_EOL;
}
?>
</div>

</div>
</body>
</html>

#10 リレーショナルデータベース

テーブル同士をつなげて(リレーション)表示させることで、多くのメリットが生まれます。 #9で2つのテーブルを作りましたが、別々に出力しておりリレーションはされていません。

氏名のあとに男性か女性を文字列で表示させましょう。
リレーションさせるためにSQL文を書き換え、echoするときに新たに取得できたデータを追記します。

<div id="drag-area">

$sql = '
  SELECT
    t1.*,
    genders.gender
  FROM
    sortable AS t1
  LEFT JOIN `genders` ON t1.gender_id = genders.id
';

$stmt = $dbh->query($sql)->fetchAll(PDO::FETCH_ASSOC);
foreach ($stmt as $result){
  echo '  <div class="drag gender'.$result['gender_id'].'" data-num="'.$result['id'].'" style="left:'.$result['left_x'].'px; top:'.$result['top_y'].'px;">'.PHP_EOL;
  echo '    <p><span class="name">'.$result['id'].' '.$result['name'].' ('.$result['gender'].')</span></p>'.PHP_EOL;
  echo '  </div>'.PHP_EOL;
}

</div>
SQL文はLEFT JOINでテーブルを結合します。
LEFT JOIN `結合させるテーブル` ON 主体テーブル.対象カラム = 結合させるテーブル.id
となります。 sortable.gender_id = genders.id という箇所は、
「sortableテーブルのgender_idと、gendersテーブルのidを同一とみなしますよ」という意味になります。

これでデータをid同士で結合(関連付け)できたので、$result['gender']という新たなデータが利用できるようになり、 gender_idが1の場合「男性」、2の場合「女性」という文字列をechoできるようになります。

SELECT文で出力したカラムが増えてきます。その際は , カンマで区切って記述しましょう。忘れるとエラーになるので注意です。

また、ここではエイリアスを使ってみましょう。
テーブル名 AS エイリアス名 でエイリアス(別名)が作れます。 FROM sortable AS t1 でt1という名前のエイリアスでSQL文を書いていきます。複数のテーブルを扱う場合は便利になります。

最後にSQL文を、phpMyAdminのSQLタブで実行してみましょう。phpで取得できるデータを、事前にみることができます。(クオーテーションの中の文章だけをペーストしましょう)


ブラウザで表示するとこのようになります。