SunShop Modifications Part 3

In this section we look at one of my custom SunShop pages, the SEO list. I’m assuming you have access to a SunShop admin area, and you know what an admin page looks like. Visit the SunShop Official Demo Page here to follow along if you need to.

The Customization

As part of our SEO (Search Engine Optimization) work, we are examining all product descriptions, short descriptions, meta descriptions, sample text, etc. I wrote code for the Edit Product page which creates a product preview, a Google-listing simulation, etc., with the target search terms highlighted.

In order to highlight the search terms, naturally, we need to know the list of target search terms. This quick-and-simple admin page allows us to enter each search term, its relative priority (1, 2, 3, etc.) on our list, and a CSS class defining the highlight color. You will recall our page support looks like this:

} elseif($_GET['type'] == 'seolist') {
        include_once $abs_path.'/'.ADMIN_DIR.'/admin.custom.seolist.php';
        $model = new CustomSEOList(array('type' => $_GET['type'], 'forms' => $forms));
        if($_GET['sub'] == 'update') {
        $out = $model->showform();

Our class definition begins:

class CustomSEOList extends CustomBase {

And indeed the parent class contains our generic constructor:

class CustomBase {
    public $parms = array();
    public $formdata = array();
    public $escaped = array();
    public static $convertToSKU = 1;

    public function __construct($parms) {
        foreach($parms as $key => $value) {
            $this->parms[$key] = $value;

Thus we can instantiate any subclass by passing in a (possibly empty) keyed array.

Our next step is loadData():

    public function loadData() {
        global $DB_site, $dbprefix, $settings;
        $sql = "SELECT * FROM ss_seolist order by rank, phrase, class limit 1000";
        $rows = $DB_site->query($sql);
        $this->seodata = array();
        while($row = $DB_site->fetch_assoc($rows)) {
            $id = (string)$row['id'];
            $this->seodata[$id] = $row;
        foreach($this->seodata as $id => $row) {
            $phrase  = $row['phrase'];
            $phrase  = preg_replace('/\s\s+/', ' ', $phrase);
            $phrase  = str_replace(' ', '\s+', $phrase);
            $pattern = "/\b($phrase)\b/i";
            $this->seodata[$id]['pattern'] = $pattern;

Lines 2-9 are the standard SunShop paradigm for a multi-row database query. In line 3 I did not bother to use the global $dbprefix for the table name prefix. I never expect more than a few dozen target SEO phrases, but put in a sanity limit of 1000 items.

Lines 10-16 convert each SEO target phrase to a PHP regular expression pattern and store the result in the object’s seodata property. Because we’ll be scanning free-form text, we form a pattern that can only match on word boundaries, and if the target phrase is multi-word, we allow “one or more whitespace characters” to be part of the phrase.

Lines 2-9 store the result set in seodata, and lines 10-16 add one more column (pattern) to that result set. We could have stored the pattern in the database itself, but this calculation is so rare that I left things as-is. We only do the calculation when updating our SEO phrase list (rare) or when editing individual products (several times per week).

Form Update

The update logic may not make much sense if you have not yet seen the form. However, by reading the update code, we know exactly what data the form must send.

    public function doUpdate() {
        $maxfieldid = (int)$_POST['maxfieldid'];
        if($maxfieldid > 0) {
            for($fieldid = 1; $fieldid <= $maxfieldid; $fieldid++) {

    public function doUpdateSEOList($fieldid) {
        $id = (int)$_POST["id_$fieldid"];
        if($id > 0) {
            $this->updateRow($fieldid, $id);
        } else {

    public function updateRow($fieldid, $id) {
        global $DB_site, $dbprefix, $settings;
        $active   = (int)$_POST["active_$fieldid"];
        $rank     = (int)$_POST["rank_$fieldid"];
        $class    = mysql_real_escape_string(trim($_POST["class_$fieldid"]));
        $phrase   = mysql_real_escape_string(trim($_POST["phrase_$fieldid"]));
        if(strlen($phrase)) {
            $sql = "UPDATE ss_seolist SET active = '$active',
                rank = '$rank', class = '$class', phrase = '$phrase'
                WHERE id = '$id'";
        } else {
            $sql = "DELETE FROM ss_seolist WHERE id = '$id'";

    public function insertRow($fieldid) {
        global $DB_site, $dbprefix, $settings;
        $active   = (int)$_POST["active_$fieldid"];
        $rank     = (int)$_POST["rank_$fieldid"];
        $class    = mysql_real_escape_string(trim($_POST["class_$fieldid"]));
        $phrase   = mysql_real_escape_string(trim($_POST["phrase_$fieldid"]));
        if(strlen($phrase)) {
            $sql = "INSERT INTO ss_seolist (active,rank,class,phrase)
            VALUES ('$active','$rank','$class','$phrase')";

Lines 1-9. I tried to have each of my custom pages follow the same structure. The doUpdate() method loops through the available data, and then reloads the object data from the database. The actual update is factored out to a separate function so that all doUpdate() methods can be clearly recognized as nearly identical.

Lines 11-19. doUpdateSEOList() takes the record ID of that HTML form row (if any). If there’s a record ID, that means we have existing data in the database and we do an Update. If not, we do an insert. The remaining lines above do the update or insert.

Display Admin Page

My showform() function, again, is nearly identical for every page. We start the form, display the data rows, display some blank rows for new target phrases, and display the update button.

    public function showform() {
        $forms = $this->parms['forms'];
        $type  = $this->parms['type'];
        $this->maxfieldid = 0;
        $return  = '';
        $do = 'update';
        $data  = $this->showSEOBlock();
        $data .= $forms->start(array('url' => "adminindex.php?action=custom&type=$type&sub=$do"));
        $data .= $this->showCurrentRecords();
        $data .= $this->showNewRecords();
        $data .= $forms->item("hidden", "", "maxfieldid", "", $this->maxfieldid);

        $data .= $forms->item("submit", "", "", "", "Update SEO List");
        $data .= $forms->end(array('hide' => 'Form'));
        //$return .= "<pre>".htmlentities(print_r($this->escaped,true))."</pre>\n";
        $return .= starttable("865", "Configure SEO List", $data, array());
        return $return;

What’s Next

In the Part 4 we look at the object registry – another mechanism for keeping the custom code “off to the side.”