1

I'll start off by saying, Yes, I've searched this... I'm reading books about programming to increase my knowledge. Right now I have not found an answer to this question. I have a working solution to my application but it looks a mess. This is PHP.

Situation: I am creating an application that includes a dynamic menu (food) that pulls from 3 tables in a database:

menu_types:
id name
1  Bar
2  Kitchen
menu_categories:
id  name      type_id
1   Salads    2
2   Teas      1
3   Pastries  2

menu_items
id  name           cat_id  description     price   image
1   Caesar         1       'hail Caesar'   $5.00   'image location'
2   Greek          1       'Plato's fav'   $3.00   'image location'
3   Cinnamon Roll  3       'lots-o-sugar'  $2.50   'image location'

There's more data, but I think you get the idea on the tables.

My menu would look something like this: (HTML page) Teas, Salads, and Pastries would be links that when clicked only show their items. Below is a rough example, let's say salads are the current page.

**Bar**           'Caesar image', Caesar, hail Caesar, $5.00
Teas              'Greek image', Greek, Plato's fav, $3.00
**Kitchen**
Salads
Pastries

Currently I build this all in one class. One object. I end up building three arrays

$this->types      = array(
                    [0] => array(
                           'id' => 1,
                           'name' => 'Bar'
                           ),
                    [1] => array(
                           'id' => 2,
                           'name' => 'Kitchen'
                           )
                    );

$this->categories = array(
                    [Bar]     => array(
                                 'id'      => 2,
                                 'name'    => 'Teas',
                                 'type_id' => 1
                                 )
                    ),
                    [Kitchen] => array(
                                 'id'      => 1,
                                 'name'    => 'Salads',
                                 'type_id' => 2
                                 ),
                                 array(
                                 'id'      => 3,
                                 'name'    => 'Patries',
                                 'type_id' => 2
                                 )
                    );

And $this->items has it's own array with $this->category[id] as it's main key.

This all gets extremely messy though. And trying to iterate through these arrays for the different parts of the menu is terrible. I didn't include the code because I feel like this whole thought process is fundamentally wrong. I'm not looking for someone to give me a full blown code example, just help me with the thought process. I think that's the hardest part of coding, thinking of a solution to the problem.

in summary I'm looking to create a dynamic menu that filters both the items based on category and the categories based on the types

for fun, here is the code I was using:

menu.php

    <?php

class menu{

  private $category;
  private $size;
  private $types;
  private $categories;
  private $items = array();

  public function __construct(){
    $this->setTypes();
    $this->setCategories();
    $this->setItems();
  }

  private function setTypes(){
    $sql = "SELECT * FROM bbc_menu_types";
    $this->types = db::rowAsAssocArray($sql);
  }

  public function getTypes(){
      return $this->types;
  }

  private function setCategories(){

    for ($i = 0; $i < count($this->types); $i++){
      $typeId = $this->types[$i]['id'];
      $sql = "SELECT * FROM bbc_menu_categories WHERE type_id = $typeId";
      $this->categories[$this->types[$i]['name']] = db::rowAsAssocArray($sql);
    }
  }

  public function getCategories(){
      return $this->categories;
  }

  public function setItems(){
    foreach ($this->categories as $key => $value){
      for ($i = 0; $i < count($value); $i++){
        $catId = $value[$i]['id'] . '<br />';
        settype($catId, 'integer');
        $sql = "SELECT * FROM bbc_menu_items WHERE category_id = $catId";
        $this->items[$this->categories[$key][$i]['name']] = db::rowAsAssocArray($sql);
      }
    }
  }

  public function getItems(){
    return $this->items;
  }
}


?>

db.pgp (excerpt)

public static function rowAsAssocArray($sql){
    try{
      //do not touch
      $data=null;
      $stmt = self::$link->prepare($sql);
      $stmt->execute();
      while ($result = $stmt->fetchAll(PDO::FETCH_ASSOC)){
        foreach ($result as $key => $value){
          $data[] = $value;
        }
      }
      return $data;
    }
    catch (PDOException $e){
      print $e->getMessage();
    }
  }

html (expert)

<?php
                      $i=0;
                      foreach ($types as $value){
                        echo "<li><h3>" . $types[$i]['name'] . "</h3></li>";
                        $i2 = 0;
                        while ($i2 <count($categories[$types[$i]['name']])){
                          if ($_GET['cat'] === strtolower($categories[$types[$i]['name']][$i2]['name'])){
                            echo "<li class='active'><a href='?rt=menu&amp;cat=";
                          } else {
                            echo "<li><a href='?rt=menu&amp;cat=";
                          }
                          echo strtolower($categories[$types[$i]['name']][$i2]['name']) . "'>" . $categories[$types[$i]['name']][$i2]['name'] . "</a></li>";
                          $i2++;
                        }
                        $i++;
                      }
                      ?>
                      </ul>
                    </nav>
                </div><!-- end menu-links -->
            </div><!-- end col -->

            <div class="col-sm-12 col-md-9">
                <div class="menu-links-big">
                    <?php
                    $i = 0;
                    while ($i < count($items[ucfirst($_GET['cat'])])){
                    ?>
                    <div class="menu-item">
                        <img src="<?php echo $items[ucfirst($_GET['cat'])][$i]['image']; ?>" alt="" class="img-thumbnail shadow alignleft">
                        <h3><?php echo $items[ucfirst($_GET['cat'])][$i]['name']; ?></h3>
                        <p><?php echo $items[ucfirst($_GET['cat'])][$i]['description']; ?></p>
                        <div class="price"><?php echo '$' . $items[ucfirst($_GET['cat'])][$i]['price']; ?></div>
                    </div><!-- end item -->
                    <?php
                    $i++;
                    }
                    ?>

Like I said... A mess... There is obvious cleaning that could be done. And there is cleaning and refactoring on top of that. I'm aware. I just finished getting ti to work before I decided to ask about it.

7
  • 4
    Did you try some existing framework? There are many of them, which have already implemented a layer which provides an OOP access to the database (e.g Symfony which is using Doctrine). Commented Jan 6, 2016 at 16:20
  • 1
    Are you using ajax to query the db to get the specific food items when a link is clicked? Commented Jan 6, 2016 at 16:21
  • Why would you need objects to store the menu when you are just taking rows out of a database and printing HTML derived from them? You need arrays, but just the temporary arrays that are the recordsets from the db your like $row array. Commented Jan 6, 2016 at 16:24
  • I have not tried a framework. I'm learning PHP. I know the frameworks exist, but until I feel like I know what I'm doing I figured staying away from them is best. I was thinking, get as much practice as I can even if it means writing out my own framework, which I'm kinda doing along with this project from various tutorials. Commented Jan 6, 2016 at 16:26
  • 2
    Even using Doctrine alone would help you through this "mess" tremendously. Organizing your code in multiple entities (Type, Category, Item) with ManyToMany anotations and simply querying those by calling their getters (eg. $someCategory->getItems() gives you an array of all the items in that category). There's literally no logic coding required on your side then, just declaring the classes and fields. Commented Jan 6, 2016 at 16:42

2 Answers 2

4

I feel like such a well-formed question deserves a longer answer.

For such a case, I'd strongly recommend the use of Doctrine.

You could then simply write your 3 entities, for example:

Type.php

<?php
// src/Type.php

use Doctrine\Common\Collections\ArrayCollection;

/** @Entity */
class Type
{
    /**
      * @Id
      * @Column(type="integer")
      * @GeneratedValue
      **/
    protected $id;

    /** @Column(type="string") **/
    protected $name;

    /** @OneToMany(targetEntity="Category", mappedBy="type") **/
    protected $categories;

    public function __construct()
    {
        $this->categories = new ArrayCollection();
    }

    public function getId()
    {
        return $this->id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    public function addCategory($category)
    {
        $category->setType($this);
        $this->categories->add($category);

        return $this;
    }

    public function removeCategory($category)
    {
        $category->setType(null);
        $this->categories->remove($category);

        return $this;
    }

    public function getCategories()
    {
        return $this->categories;
    }
}

Category.php

<?php
// src/Category.php

use Doctrine\Common\Collections\ArrayCollection;

/** @Entity */
class Category
{
    /**
      * @Id
      * @Column(type="integer")
      * @GeneratedValue
      **/
    protected $id;

    /** @Column(type="string") **/
    protected $name;

    /**
     * @ManyToOne(targetEntity="Type", inversedBy="categories")
     * @JoinColumn(name="type_id", referencedColumnName="id")
     */
    protected $type;

    /** @OneToMany(targetEntity="Item", mappedBy="category") **/
    protected $items;

    public function __construct()
    {
        $this->items = new ArrayCollection();
    }

    public function getId()
    {
        return $this->id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    public function getType()
    {
        return $this->type;
    }

    public function setType($type)
    {
        $this->type = $type;
    }

    public function addItem($item)
    {
        $item->setCategory($this);
        $this->items->add($item);

        return $this;
    }

    public function removeItem($item)
    {
        $item->setCategory(null);
        $this->items->remove($item);

        return $this;
    }

    public function getItems()
    {
        return $this->items;
    }
}

Item.php

<?php
// src/Item.php

use Doctrine\Common\Collections\ArrayCollection;

/** @Entity */
class Item
{
    /**
      * @Id
      * @Column(type="integer")
      * @GeneratedValue
      **/
    protected $id;

    /** @Column(type="string") **/
    protected $name;

    /**
     * @ManyToOne(targetEntity="Category", inversedBy="items")
     * @JoinColumn(name="category_id", referencedColumnName="id")
     */
    protected $category;

    public function getId()
    {
        return $this->id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    public function getCategory()
    {
        return $this->category;
    }

    public function setCategory($category)
    {
        $this->category = $category;

        return $this;
    }
}

After setting all this up Doctrine will generate your tables and you can simply query your information like this:

$repo = $entityManager->getRepository("Type");
$kitchen = $repo->findOneBy(array("name" => "Kitchen"));

echo "Fetching 'Kitchen'-Categories...<br/>";
$categories = $kitchen->getCategories();
foreach($categories as $category)
{
    $itemsInCategory = $category->getItems();

    echo "Category: ".$category->getName().", Items:<br/>";
    foreach($itemsInCategory as $item)
    {
        echo $item->getName()."<br/>";"
    }
}

As you see, at that point it's simply a matter of calling getters to query your whole tree.

Here are two example files to show you the usage:

Creating/Storing objects:

<?php
// createTest.php
require_once "bootstrap.php";

// Create 'Bar' Type
$bar = new Type();
$bar->setName("Bar");
$entityManager->persist($bar);

// Create 'Kitchen' Type
$kitchen= new Type();
$kitchen->setName("Kitchen");
$entityManager->persist($kitchen);


// Create 'Salads' Category...
$salads = new Category();
$salads->setName("Salads");
$entityManager->persist($salads);

// ... and add it to the kitchen
$kitchen->addCategory($salads);

// Create Teas and add to Bar
$teas = new Category();
$teas->setName("Teas");
$entityManager->persist($teas);

$bar->addCategory($teas);

// Create Pastries and add to Kitchen
$pastries = new Category();
$pastries->setName("Pastries");
$entityManager->persist($pastries);

$kitchen->addCategory($pastries);

// Add some Items
$caesar = new Item();
$caesar->setName("Caesar Salad");
$entityManager->persist($caesar);

$salads->addItem($caesar);


$entityManager->flush();

Querying objects:

<?php
// queryTest.php
require_once "bootstrap.php";

$typeRepo = $entityManager->getRepository("Type");

$types = $typeRepo->findAll();

echo "Found ".count($types)." types:\n";
foreach($types as $type)
{
    echo $type->getName()."\n";

    foreach($type->getCategories() as $category)
    {
        echo " - ".$category->getName()."\n";

        foreach($category->getItems() as $item)
        {
            echo "   - ".$item->getName()."\n";
        }
    }
}

Output:

Found 2 types:
Bar
 - Teas
Kitchen
 - Salads
   - Caesar Salad
 - Pastries

You can do all kinds of shenanigans at this point and adding more properties/methods to your classes is just a matter of declaring them, providing a solid doctrine annotation and updating the database.

The added benefit is that you're database independent, the example uses sqlite since it's easy to setup but you could just aswell use mysql or any other PDO supported database. Doctrine also takes care about SQL injection since it's using prepared statements.

For more detailed examples take a look at the documentation linked at top!

Sign up to request clarification or add additional context in comments.

3 Comments

Oh wow, thanks! I'll certainly check this out. Appreciate the time and effort!
Just tested the code and it seems to work just fine, added examples (+output) at the bottom.
thanks again! I think I'll use this approach. I can read it for one... And when I come back to make changes it will be tons easier. Guess that means it's time to start using frameworks. I'll have to read all that documentation on top of the books I'm already reading zzzz...
1

It is as complicated as you make it for yourself. :-)

Your data is structured (in the database) in a neat normalized fashion. Cheers to that!

Now you must represent it in a nice way in PHP.

I would use an array to represent it all (unless you have over a million entries, in that case you need a different approach).

We only need a way to find to remember the menu_types.id for each menu_categories.id. I use a simple array for that: $remember_menuID

// assuming you use associative queries

$all = array();
$RS = $db->GetAll("SELECT id, name FROM menu_types;");
foreach ($RS as $one){
  $all[$one['id']] = array('menu_type' => $one['name'],
                            'menu_categories' => array());
}

// fetch all categories
$remember_menuID = array(); // stores the menu_types.id for each menu_categories.id
$RS = $db->GetAll("SELECT id, name, type_id FROM menu_categories;");
foreach ($RS as $one){
  $all[$one['type_id']]['menu_categories'][$one["id"]] = array('category_name' => $one['name'],
                                                              'menu_items' => array());
  $remember_menuID[$one['id']] = $one['type_id'];
}

// and at last the menu_items
$RS = $db->GetAll("SELECT id, name, cat_id, description, price, image FROM menu_items;");
foreach ($RS as $one){
  // lookup menu_type belonging to this cat_id
  $menu_type_id = $remember_menuID[$one['cat_id']];
  $all[$menu_type_id]['menu_categories'][$one["cat_id"]]['menu_items'][] = %one;
}

echo "<pre>";
print_r($all);
echo "<pre>";

Not tested.

1 Comment

Well that's seems similar to what I have, but cleaner.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.