0xC0CAC0DA

Rants and code from PolCPP.

How to Install and Use Laravel Administrator Part 2

In our previous post we installed Laravel 4.2, created a very simple auth system and configured Frozennode administrator to edit the users model, in this second part we’ll build a real(ish) world case creating a model with some relationships and see how it performs on the administrator.

First, so you can know about the app structure, we’re talking about a small part of a remake of this site under Laravel. In this case we’re talking about 2 tables and pivot one linking them. Enemy and Item and a pivot table is Enemy item drops. An enemy can drop one of multiple items, and items can be drop by many enemies.

Enemy

  • name: string
  • name_jp: string
  • is_rare: string
  • image: string

Item

  • name: string
  • name_jp: string
  • desc: text
  • desc_jp: text
  • rarity: integer
  • max_stack_on_hand: integer
  • max_stack_in_storage: integer
  • is_arks_cash: boolean
  • is_tradable: boolean
  • account_bound_on_equip: boolean
  • is_account_bound: boolean
  • can_be_dropped: boolean
  • can_be_sold: boolean
  • can_be_exchanged: boolean
  • can_be_fed_to_mag: boolean
  • buy_price: integer
  • sell_price: integer
  • notes: text
  • image: string

Drop

  • enemy_id: integer
  • item_id: integer
  • drop_rate: float

Now that we know what we’re gonna build let’s start creating those database migrations.

php artisan generate:migration create_enemies_table --fields="name:string, name_jp:string, is_rare:string, image:string"

php artisan generate:migration create_items_table --fields="name:string, name_jp:string, desc:text, desc_jp:text, rarity:integer, max_stack_on_hand:integer, max_stack_in_storage:integer, is_arks_cash:boolean, is_tradable:boolean, account_bound_on_equip:boolean, is_account_bound:boolean, can_be_dropped:boolean, can_be_sold:boolean, can_be_exchanged:boolean, can_be_fed_to_mag:boolean, buy_price:integer, sell_price:integer, notes:text, image:string"

php artisan generate:pivot enemies items

When running migrations using Jeffrey Way’s generators make sure there’s a space between the comma and the next field like this “name:string, name_jp:string”. If you input something like “name:string,name_jp:string” you’ll get a weird output.

Since Laravel generators don’t allow us to add fields directly on pivot tables let’s open our pivot table migration on app/database/migrations/ and add the drop_rate value and leave the up function like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
public function up()
{
  Schema::create('enemy_item', function(Blueprint $table)
  {
      $table->increments('id');
      $table->integer('enemy_id')->unsigned()->index();
      $table->foreign('enemy_id')->references('id')->on('enemies')->onDelete('cascade');
      $table->integer('item_id')->unsigned()->index();
      $table->foreign('item_id')->references('id')->on('items')->onDelete('cascade');
      $table->float('drop_rate');
      $table->timestamps();
  });
}

With our schema built, let’s migrate our database and start generating our models.

php artisan migrate
php artisan generate:model Item
php artisan generate:model Enemy

And now we’ll add the relationships on both of them.

So app/models/ Enemy.php and Item.php will end like this respectively.

1
2
3
4
5
6
7
8
9
10
<?php

class Enemy extends \Eloquent {
  protected $fillable = [];

  public function drops()
  {
      return $this->belongsToMany('Item')->withPivot('drop_rate');
  }
}
1
2
3
4
5
6
7
8
9
10
11
<?php

class Item extends \Eloquent {
  protected $fillable = [];

  public function dropped_by()
  {
      return $this->belongsToMany('Enemy')->withPivot('drop_rate');
  }

}

Probably you’re wondering whats that withPivot doing. By default getting a privot table using the –>pivot attribute (you’ll see it in action the next example) only gets the keys, so we need to directly specify we want the drop rate.

Now let’s see how these in action just using code (you can skip this since it’s not related to the admin but just an example of how this works).

Replace the / route with this new function on /app/routes.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Route::get('/', function()
{
  // We need this we're using mass assignment to create the enemy/item.
  Eloquent::unguard();

  // Let's create an enemy
  $enemy = Enemy::firstOrCreate(array('name' => 'TestEnemy'));

  // And now a drop
  $item = Item::firstOrCreate(array('name' => 'TestDrop'));

  // Now let's add our item to the drop table
  $enemy->drops()->attach($item);

  // In case we want to add it with a specific drop rate.
  $enemy->drops()->attach($item, array('drop_rate' => 12.34));

  // Now imagine we want to see all our drops and their drop rate. 
  foreach ($enemy->drops as $drop)
  {
      echo $drop->name . ": drop rate is: " . $drop->pivot->drop_rate . "<br/>";
  }

  // Now let's get rid of all the drops. 
  // This does not delete anything from the item table
  // only gets rid of the relation.
  $enemy->drops()->detach(); 
});

I think the code is self explanatory with those comments, but in TLDR, we create an item, an enemy then we link them using attach, twice (one without drop rate and another with it), we display them and then we clean the relationships.

If you run the base url now you’ll probably see something like:

TestDrop: drop rate is: 0.00
TestDrop: drop rate is: 12.34

With that simple example done let’s build our admin panel. To do that we’ll need to create the item.php and enemies.php in /app/config/administrator/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
  <?php

  /**
   * Item model config
   */

  return array(

      'title' => 'Items',
      'single' => 'item',
      'model' => 'Item',

      'columns' => array(
          'name' => array(
              'title' => 'Name',
          ),
          'name_jp' => array(
              'title' => 'JP Name',
          ),          
          'rarity' => array (
              'title' => 'Rarity'
          ),
          'image' => array (
              'title' => 'Image',
              'output' => '<img src="/uploads/items/(:value)" height="100" />',
          ),
          'drops' => array (
              'title' => 'Dropped by',
              'relationship' => 'droppedBy',
              'select' => 'COUNT((:table).id)'
          )
      ),

      'edit_fields' => array(
          'name' => array(
              'title' => 'Name',
              'type' => 'text' 
          ),
          'name_jp' => array(
              'title' => 'JP Name',
              'type' => 'text' 
          ),
          'desc' => array(
              'title' => 'Description',
              'type' => 'markdown' 
          ),
          'desc_jp' => array(
              'title' => 'JP Description',
              'type' => 'markdown' 
          ),
          'rarity' => array(
              'title' => 'Rarity',
              'type' => 'number'
          ),
          'max_stack_on_hand' => array(
              'title' => 'Max stack on hand',
              'type' => 'number'
          ),
          'max_stack_in_storage' => array(
              'title' => 'Max stack on storage',
              'type' => 'number' 
          ),
          'is_arks_cash' => array(
              'title' => 'Is an arks cash item',
              'type' => 'bool' 
          ),
          'is_tradable' => array(
              'title' => 'Is tradable',
              'type' => 'bool' 
          ),
          'account_bound_on_equip' => array(
              'title' => 'Bound on equip',
              'type' => 'bool' 
          ),
          'is_account_bound' => array(
              'title' => 'Is account bound',
              'type' => 'bool' 
          ),
          'can_be_dropped' => array(
              'title' => 'Can be dropped',
              'type' => 'bool' 
          ),
          'can_be_sold' => array(
              'title' => 'Can be sold',
              'type' => 'bool' 
          ),
          'can_be_exchanged' => array(
              'title' => 'Can be exchanged',
              'type' => 'bool' 
          ),
          'can_be_fed_to_mag' => array(
              'title' => 'Can be fed to mag',
              'type' => 'bool' 
          ),
          'buy_price' => array(
              'title' => 'Buy price',
              'type' => 'number' 
          ),
          'sell_price' => array(
              'title' => 'Sell price',
              'type' => 'number' 
          ),
          'notes' => array(
              'title' => 'Notes',
              'type' => 'markdown' 
          ),
          'image' => array(
              'title' => 'Image',
              'type' => 'image',
              'location' => public_path() . '/uploads/items/',
          ),
          'droppedBy' => array(
              'type' => 'relationship',
              'title' => 'Dropped by',
              'name_field' => 'name',
          )
      ),

      'filters' => array(
          'name' => array(
              'title' => 'Name',
              'type' => 'text' 
          ),
          'rarity' => array(
              'title' => 'Rarity',
              'type' => 'number'
          ),
          'max_stack_on_hand' => array(
              'title' => 'Max stack on hand',
              'type' => 'number'
          ),
          'max_stack_in_storage' => array(
              'title' => 'Max stack on storage',
              'type' => 'number' 
          ),
          'is_arks_cash' => array(
              'title' => 'Is an arks cash item',
              'type' => 'bool' 
          ),
          'is_tradable' => array(
              'title' => 'Is tradable',
              'type' => 'bool' 
          ),
          'account_bound_on_equip' => array(
              'title' => 'Bound on equip',
              'type' => 'bool' 
          ),
          'is_account_bound' => array(
              'title' => 'Is account bound',
              'type' => 'bool' 
          ),
          'can_be_dropped' => array(
              'title' => 'Can be dropped',
              'type' => 'bool' 
          ),
          'can_be_sold' => array(
              'title' => 'Can be sold',
              'type' => 'bool' 
          ),
          'can_be_exchanged' => array(
              'title' => 'Can be exchanged',
              'type' => 'bool' 
          ),
          'can_be_fed_to_mag' => array(
              'title' => 'Can be fed to mag',
              'type' => 'bool' 
          ),
          'droppedBy' => array(
              'type' => 'relationship',
              'title' => 'Dropped by',
              'name_field' => 'name',
          )
      ),
  );      

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
  <?php

  /**
   * Enemy model config
   */

  return array(

      'title' => 'Enemies',
      'single' => 'enemy',
      'model' => 'Enemy',

      'columns' => array(
          'name' => array(
              'title' => 'name' 
          ),
          'name_jp' => array(
              'title' => 'name_jp' 
          ),
          'is_rare' => array(
              'title' => 'is_rare' 
          ),
          'image' => array(
              'title' => 'image',
              'output' => '<img src="/uploads/items/(:value)" height="100" />',
          ),
          'drops' => array(
              'title' => 'enemy drops',
              'relationship' => 'drops',
              'select' => 'COUNT((:table).name)'
          )
      ),

      'edit_fields' => array(
          'name' => array(
              'title' => 'Name', 
              'type' => 'text' 
          ),
          'name_jp' => array(
              'title' => 'JP Name', 
              'type' => 'text' 
          ),
          'is_rare' => array(
              'title' => 'Is rare?', 
              'type' => 'bool' 
          ),
          'image' => array(
              'title' => 'Image', 
              'type' => 'image',
              'location' => public_path() . '/uploads/items/'
           ),
          'drops' => array(
              'type' => 'relationship',
              'title' => 'Drops',
              'name_field' => 'name',
          )
      ),


      'filters' => array(
          'name' => array(
              'title' => 'Name', 
              'type' => 'text' 
          ),
          'is_rare' => array(
              'title' => 'Is rare?', 
              'type' => 'bool' 
          ),
          'drops' => array(
              'type' => 'relationship',
              'title' => 'Drops',
              'name_field' => 'name',
          )           
      ),

  );

After creating those two files remember to add the item and enemy values to the menu array in app/config/packages/frozennode/administrator/administrator.php

It’s possible that you will be getting the following exception when going to the Item and Enemy admin pages: BadMethodCallException Call to undefined method Illuminate\Database\Query\Builder::isSoftDeleting() this is a small issue with Administrator and laravel 4.2, so depending if they patch this up you’ll probably need a newer version of administrator or apply this patch by yourself. Make sure you read my comment on that commit, or you’ll find problems later on.

With these possible issues resolved let’s explain this a bit. As with the Users models from our previous tutorial, we have two big sections, columns and edit_fields, but now we also have a filters section.

If you’re wondering filters work, it’s exactly the same as the edit_fields, except the image type field does not work with them (you couldn’t filter by image anyway).

We’re gonna center ourselves on the relationships, since it’s the most complicated but if you want to see details of the other options i’ve set for the columns (like the image preview) check this documentation link and if you’re curious about the edit_fields types check this one and the Field Types submenus on the left side of that page. Their docs are really neat and with clear examples. So let’s get into the relationships.

Getting back into our column view, to get data from related columns we use this.

1
2
3
4
5
'drops' => array(
  'title' => 'enemy drops',
  'relationship' => 'drops',
  'select' => 'COUNT((:table).name)'
)

We have two new mandatory options here. Relationship is always the method we created on the model and select works like a query. The (:table) value will always reference to the related table.

Also it will affect all values on toMany relationships so it returns a field on toOne ones. What does this mean? In our case doing ‘(:table).name’ wouldn’t return anything since it would return one or more than one field, so we use a SQL aggregation function. But if we had a one to one relationship we could return the name (we’ll see an example soon).

Now let’s see how this works on edit_fields.

1
2
3
4
5
'drops' => array(
  'type' => 'relationship',
  'title' => 'Drops',
  'name_field' => 'name',
)  

As a type we have relationship, and then we have a value named name_field. Thats the column from the related table that you want to show to the user. In our case since the item name is the most useful to assign things we use it.

If you see we also use the ‘drops’ model method as the array key to define the column.

Now, you may have realised that with this setup we can’t set the drop rate values on the pivot table.

That means that we need to setup a new administrator view to be able to set those views and a new model. So lets build them.

php artisan generate:model EnemyItem

Time to edit the model.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class EnemyItem extends \Eloquent {
  protected $fillable = [];
    protected $table = 'enemy_item';

  public function Enemy()
  {
      return $this->belongsTo('Enemy');
  }

  public function Item()
  {
      return $this->belongsTo('Item');
  }

}

Since it’s a pivot table we add the enemy and item relationships. Now let’s create a “enemyitems.php” admin menu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
  <?php

      /**
       * EnemyItems model config
       */

      return array(

          'title' => 'Drops',
          'single' => 'drop',
          'model' => 'EnemyItem',

          'columns' => array(
              'drop_rate' => array(
                  'title' => 'Drop Rate',
              ),
              
              'enemy' => array (
                  'title' => 'Dropped by',
                  'relationship' => 'Enemy',
                  'select' => '(:table).name'
              ),

              'item' => array (
                  'title' => 'Item',
                  'relationship' => 'Item',
                  'select' => '(:table).name'
              )
          ),

          'edit_fields' => array(
              'drop_rate' => array(
                  'title' => 'Drop rate',
                  'type' => 'number' 
              ),
              
              'enemy' => array(
                  'type' => 'relationship',
                  'title' => 'Dropped by',
                  'name_field' => 'name',
              ),

              'item' => array(
                  'type' => 'relationship',
                  'title' => 'Drop',
                  'name_field' => 'name',
              )
          ),

          'filters' => array(
              'enemy' => array(
                  'type' => 'relationship',
                  'title' => 'Dropped by',
                  'name_field' => 'name',
              ),
              'item' => array(
                  'type' => 'relationship',
                  'title' => 'Drop',
                  'name_field' => 'name',
              ),
          ),

      );

As you can see it’s pretty similar but this is the case where on our relations we can use ‘select’ => ‘(:table).name’ on the column name and we’ll get the Item name and the enemy name since our relationship is many to one on both item and enemy.

And finally, add this new administrator to the menu config array on administrator.php and we’re done

Before finishing just some tips.

  • Administrator lets you do more things with relationships so make sure you check the documentation to get a more deeper insight, but what we’ve been covering here should be useful for basic admin panels.

  • While it looks like we did a lot of code, if you use sublime text, it’s multicursor is extremely useful to create this really fast from the field lists we had at the beginning of this post.

  • As i said on the first part of this tutorial if you want to use sentry or any auth library with frozennode administrator you can without issues, just check this to see how to do it.

That’s all, we’re done with this two part tutorial. I Hope it was useful.

As with the previous one you can access the repo of this tutorial by clicking here