Jump to content



Photo
- - - - -

How to make a 2.4 App


  • Please log in to reply
33 replies to this topic

#1   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 14 March 2017 - 23:58

I'm going to attempt to make an app, and I'm going to post my methods here so that others can follow along and hopefully join in. With a bit of luck @Harald Ponce de Leon and others better at PHP coding will see this and make any corrections or give some help where needed.

 

Of course, all this is speculative as we don't know what the final form of oSC 2.4 will be, but as the Paypal App is in there and presumably is more or less complete we can use that as a template.

 

I am going to make a Related Products App, for two reasons. It is fairly simple, but also involves configuration, admin pages, and at least one content module.

 

OK, here goes:

 

First, let's get make the most basic module possible, one that put some static content on the product_info page.

Create a new directory in catalog\includes\OSC\Apps named Test

Then create a directory structure off that, so that you have

 

 

 

includes
    |_ OSC
        |_ Apps
            |_ Test
                |_ Test
                    |_ Module
                        |_ Content
                            |_ templates

 

 

 

In OSC\Apps\Test\Test, create a file named oscommerce.json

This will tell osCommerce where to find the app, who wrote it, and where the modules and routes can be found.

In the oscommerce.json file we will be giving our app the name of Test, and the content module, which shall be called APPTEST, will belong to the product_info group.

Put the following in this file and save it

{
    "title":     "Test App",
    "app":       "Test",
    "vendor":    "Test",
    "version":    "0.0.1",
    "req_core_version":    "2.4",
    "license":    "MIT",
    "authors": [
        {
            "name":        "Author",
            "company":    "Company",
            "email":    "email@Example.com",
            "website":    "http://www.example.com"
        }
    ],
    "modules": {
        "Content": {
            "product_info": {
                "APPTEST":    "Module\\Content\\APPTEST"
            }
        }
    }
}

Create another file in the OSC\Apps\Test\Test directory called Test.php. This will initiate the Test class so it can be used by osCommerce. For now it won't do anything, but we'll add some functions to it later.

Put the following in this file

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test;

class Test extends \OSC\OM\AppAbstract
{

    protected function init()
    {
    }

  }
?>

Now we will create the APPTEST module in OSC/Apps/Test/Test/Module/Content. This app will add some static (for now) content to the product info page. Create a file in this directory called APPTEST.php. I'm not sure if capitalization of the filename is important but as that's what the Paypal app does I'm going to do it too.

Add to this file:

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

  namespace OSC\Apps\FrankL\Test\Module\Content;

  class APPTEST implements \OSC\OM\Modules\ContentInterface {
    public $code, $group, $title, $description, $sort_order, $enabled, $app;

    function __construct() {

      $this->app = 'Test';
      $this->code = 'APPTEST';
      $this->group = 'product_info';

      $this->title = 'Test App';
      $this->description = 'Just a test.';
      $this->sort_order = '10';
      
      $this->enabled = true;
    }

    function execute() {
      global $oscTemplate;


      ob_start();
      include(__DIR__ . '/templates/APPTEST.php');
      $template = ob_get_clean();

      $oscTemplate->addContent($template, $this->group);
    }
    
    function isEnabled() {
      return $this->enabled;
    }

    function check() {
    }

    function install() {
    }

    function remove() {
    }

    function keys() {
    }
  }
?>

We also need a template to display the actual content, so create a file in OSC/Apps/Test/Test/Module/Content/templates also called APPTEST.php and put the following content in it. This is just placeholder content, we'll change that to dynamically generated content later.

  <br />
  <div itemscope itemtype="http://schema.org/ItemList">
    <meta itemprop="itemListOrder" content="http://schema.org/ItemListUnordered" />
    <meta itemprop="numberOfItems" content="6" />

    <h3 itemprop="name">Test Content for Test App</h3>

    <div class="row">
      <div class="col-sm-6 col-md-4">  
        <div class="thumbnail"><a href="product_info.php?products_id=23"><img src="images/gt_interactive/wheel_of_time.gif" alt="The Wheel Of Time" title="The Wheel Of Time" width="100" height="80" /></a>
             <div class="caption"><h5 class="text-center" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"><a itemprop="url" href="product_info.php?products_id=23"><span itemprop="name">The Wheel Of Time</span></a><meta itemprop="position" content="1" /></h5>    
          </div>  
        </div>
      </div>
      <div class="col-sm-6 col-md-4">  
        <div class="thumbnail"><a href="product_info.php?products_id=1"><img src="images/matrox/mg200mms.gif" alt="Matrox G200 MMS" title="Matrox G200 MMS" width="100" height="80" /></a>    
          <div class="caption"><h5 class="text-center" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"><a itemprop="url" href="product_info.php?products_id=1"><span itemprop="name">Matrox G200 MMS</span></a><meta itemprop="position" content="2" /></h5>    
          </div>  
        </div>
      </div>
      <div class="col-sm-6 col-md-4">  
        <div class="thumbnail"><a href="product_info.php?products_id=2"><img src="images/matrox/mg400-32mb.gif" alt="Matrox G400 32MB" title="Matrox G400 32MB" width="100" height="80" /></a>    
          <div class="caption"><h5 class="text-center" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"><a itemprop="url" href="product_info.php?products_id=2"><span itemprop="name">Matrox G400 32MB</span></a><meta itemprop="position" content="3" /></h5>    
          </div>  
        </div>
      </div>
      <div class="col-sm-6 col-md-4">  
        <div class="thumbnail"><a href="product_info.php?products_id=25"><img src="images/microsoft/intkeyboardps2.gif" alt="Microsoft Internet Keyboard PS/2" title="Microsoft Internet Keyboard PS/2" width="100" height="80" /></a>    
          <div class="caption"><h5 class="text-center" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"><a itemprop="url" href="product_info.php?products_id=25"><span itemprop="name">Microsoft Internet Keyboard PS/2</span></a><meta itemprop="position" content="4" /></h5>    
          </div>  
        </div>
      </div>
      <div class="col-sm-6 col-md-4">  
        <div class="thumbnail"><a href="product_info.php?products_id=26"><img src="images/microsoft/imexplorer.gif" alt="Microsoft IntelliMouse Explorer" title="Microsoft IntelliMouse Explorer" width="100" height="80" /></a>    
          <div class="caption"><h5 class="text-center" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"><a itemprop="url" href="product_info.php?products_id=26"><span itemprop="name">Microsoft IntelliMouse Explorer</span></a><meta itemprop="position" content="5" /></h5>    
          </div>  
        </div>
      </div>
      <div class="col-sm-6 col-md-4">  
        <div class="thumbnail"><a href="product_info.php?products_id=27"><img src="images/hewlett_packard/lj1100xi.gif" alt="Hewlett Packard LaserJet 1100Xi" title="Hewlett Packard LaserJet 1100Xi" width="100" height="80" /></a>    
          <div class="caption"><h5 class="text-center" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem"><a itemprop="url" href="product_info.php?products_id=27"><span itemprop="name">Hewlett Packard LaserJet 1100Xi</span></a><meta itemprop="position" content="6" /></h5>    
          </div>  
        </div>
      </div>    
    </div>
  </div>

That's our basic content created, but it won't show on the product info page until it gets installed.

The app is not yet installed, but if you want to see if the module works so far go to phpMyAdmin and search for the MODULE_CONTENT_INSTALLED configuration_key in the configuration table. Add at the end of the list of modules installed

;product_info/Test\Test\APPTEST

then navigate to product_info.php where you should see the Test App's contents.

 

Next I'm going to attempt to create an admin menu and installation and app configuration pages.


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#2   Dan Cole

Dan Cole
  • Community Sponsor
  • 1,647 posts
  • Real Name:Dan Cole
  • Gender:Male
  • Location:Ontario, Canada

Posted 15 March 2017 - 01:01

@frankl I'm looking forward to following this thread Frank. Are you open to questions as you go along?

 

Dan



#3   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 15 March 2017 - 01:37

@Dan Cole

 

Open to questions Dan, not sure if I will be able to answer them :D


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#4   Dan Cole

Dan Cole
  • Community Sponsor
  • 1,647 posts
  • Real Name:Dan Cole
  • Gender:Male
  • Location:Ontario, Canada

Posted 15 March 2017 - 01:44

@Dan Cole

 

Open to questions Dan, not sure if I will be able to answer them :D

 

I can relate to that...the new app process raises lots of questions....I'm sure you'll be able this one.

 

Why the FrankL in the namespace declaration?

 

namespace OSC\Apps\FrankL\Test\Module\Content;

 

It's not in your directory path anywhere that I can see.  Remember you're talking to a guy who knows nothing about namespaces except that Harald seems to like them. :)

 

Dan



#5   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 15 March 2017 - 01:47

@Dan Cole

 

Got my apps mixed up -- that line should be namespace OSC\Apps\Test\Test\Module\Content

 

For those following along, please change it in your file.


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#6   Dan Cole

Dan Cole
  • Community Sponsor
  • 1,647 posts
  • Real Name:Dan Cole
  • Gender:Male
  • Location:Ontario, Canada

Posted 15 March 2017 - 02:46

@Dan Cole

 

Got my apps mixed up -- that line should be namespace OSC\Apps\Test\Test\Module\Content

 

For those following along, please change it in your file.

 

Thanks for that Frank....now I understand. :D

 

To get myself up to speed on namespaces I googled for it and found this nice explanation.

 

Dan

 

PS:  I actually understood most of it. :wacko:


Edited by Dan Cole, 15 March 2017 - 02:48.


#7   Harald Ponce de Leon

Harald Ponce de Leon

    Healthy Giraffe

  • Core Team
  • 5,346 posts
  • Real Name:Harald Ponce de Leon
  • Gender:Male
  • Location:Solingen, Germany

Posted 15 March 2017 - 02:51

@frankl you should actually use a real Vendor and App name for your example instead of Test Test, and make a note that developers must use their own Vendor and App code names (registrations will open for this when the Apps Marketplace goes live).

 

There is also a Content Module hook you can define in your json metadata file. This should automatically load your module - you just need to take care of an administration page if parameters are to be set.

 

In case anyone has missed it, some documentation is up at:

 

https://library.osco...developers


:heart: , osCommerce


#8   mcmannehan

mcmannehan
  • Members
  • 125 posts
  • Real Name:Manfred Wedel
  • Gender:Male
  • Location:Thailand

Posted 15 March 2017 - 03:07

@frankl

Nice done. Good work,  you should be start to get a teacher. You save some people a lot of time.


- The clever one learn from everything and from everybody.

- The normal one learn from his experience.

- The silly one knows everything better.

[Socrates, 412 before Christ]

 

Computers help us with the problems we wouldn't have without them!
My programmed add-ons: WDW EasyTabs 1.0.2, WDW Facebook Like 1.0.0


#9   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 15 March 2017 - 22:17

As @Harald Ponce de Leon said, when you go to make an actual app you will need vendor and app names to be your own, don't use Test for either unless it's strictly for testing purposes. Once this app is complete it will be packaged up under a different Vendor and App name.

 

Apps rely on a single admin page to administer the app. In this case it is a file called Home.php, with several controllers that take the required actions. You can think of it like php pages that show different things depending on the URL such as the categories.php page which has different actions depending on the variables (i.e. https://www.example....ion=new_product will edit Unreal Tournament in a fresh installation)

However the admin page does not have all the coding on one page like categories.php, rather it has separate classes in an Actions folder which take care of installation, configuration, and administration.

The main admin page also has templates which it accesses depending upon what actions the page needs to undertake.

We tell osCommerce which admin page to use through the oscommerce.json file in the "routes" section.

Apps also need an admin menu, which we also notify osCommerce of using another section in oscommerce.json called "AdminMenu".

Change oscommerce.json from
 

{
    "title":     "Test App",
    "app":       "Test",
    "vendor":    "Test",
    "version":    "0.0.1",
    "req_core_version":    "2.4",
    "license":    "MIT",
    "authors": [
        {
            "name":        "Author",
            "company":    "Company",
            "email":    "email@Example.com",
            "website":    "http://www.example.com"
        }
    ],
    "modules": {
        "Content": {
            "product_info": {
                "APPTEST":    "Module\\Content\\APPTEST"
            }
        }
    }
}


to
 

{
    "title":     "Test App",
    "app":       "Test",
    "vendor":    "Test",
    "version":    "0.0.1",
    "req_core_version":    "2.4",
    "license":    "MIT",
    "authors": [
        {
            "name":        "Author",
            "company":    "Company",
            "email":    "email@Example.com",
            "website":    "http://www.example.com"
        }
    ],
    "modules": {
        "AdminMenu": {
            "Test":    "Module\\Admin\\Menu\\Test"
        },
        "Content": {
            "product_info": {
                "APPTEST":    "Module\\Content\\APPTEST"
            }
        }
},
    "routes": {
        "Admin":    "Sites\\Admin\\Pages\\Home",
        "Shop": {
        }
    }
}

We've added an AdminMenu entry and a Routes/Admin entry

Apps also load their own language definitions, found in OSC/Apps/Test/Test/languages/YOUR LANGUAGE HERE, so go ahead and create that folder now (I used english, so it's OSC/Apps/Test/Test/languages/english).

To make an admin menu, create a folder OSC/Apps/Test/Test/Module/Admin/Menu

In that folder, create a file called Test.php and add
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Module\Admin\Menu;

use OSC\OM\Registry;

use OSC\Apps\Test\Test\Test as TestApp;

class Test implements \OSC\OM\Modules\AdminMenuInterface
{
    public static function execute()
    {
        if (!Registry::exists('Test')) {
            Registry::set('Test', new TestApp());
        }

        $OSCOM_Test = Registry::get('Test');

        $OSCOM_Test->loadDefinitions('admin/modules/boxes/test');

        $test_menu = [
            [
                'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                'title' => $OSCOM_Test->getDef('module_admin_menu_install'),
                'link' => $OSCOM_Test->link()
            ]
        ];

        return array('heading' => $OSCOM_Test->getDef('module_admin_menu_title'),
                     'apps' => $test_menu);
    }
}


You may notice that the menu file makes use of aliasing, in this line use OSC\Apps\Test\Test\Test as TestApp;

All that means is that we are telling osCommerce that the full path to the class is OSC\Apps\Test\Test\Test (.php extension assumed) but it can reference it with the shorter alias of Testapp.

You can also see a new function loadDefinitions, which will grab the language defines for the menu from the file admin/modules/boxes/test (assumes .txt so you don't need to add that) in whatever language your admin is set to (in this case, we only have english)

The accompanying language file will be created in OSC/Apps/Test/Test/languages/english/admin/modules/boxes

In that folder create a file test.txt and add:

module_admin_menu_title = Test App
module_admin_menu_install = Install


At the moment we just have Install, we'll add more menu definitions later.

The link shown in the line 'link' => $OSCOM_Test->link() is the one we defined earlier in the oscommerce.json file under routes, Sites\\Admin\\Pages\\Home

That file, Home.php, is created in a folder OSC/Apps/Test/Test/Sites/Admin/Pages/Home

Put in that file
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home;

use OSC\OM\Apps;
use OSC\OM\OSCOM;
use OSC\OM\Registry;

use OSC\Apps\Test\Test\Test;

class Home extends \OSC\OM\PagesAbstract
{
    public $app;

    protected function init()
    {
       
         $OSCOM_Test = new Test();
        Registry::set('Test', $OSCOM_Test);

        $this->app = $OSCOM_Test;
        
        $this->app->loadDefinitions('admin/install');
        
    }
}


This file on it's own won't do much.

We do have to make some more definitions though, it's asking for language definitions from admin/install so in the OSC/Apps/Test/Test/languages/english/admin folder create a file named install.txt (remembering that in the line $this->app->loadDefinitions('admin/install'); a .txt extension is assumed)

Put in that file
 

onboarding_intro_title = Related Products
onboarding_intro_body = <p>Manage related products and display in your store to drive further sales</p><p style="text-align: center;">{{button_install}}</p>

button_install = Install Now

We'll use those definitions in a template file in a moment.

We now need templates for the Home.php page to use. These go in the folder OSC/Apps/Test/Test/Sites/Admin/Pages/Home/templates

template_top.php
 

<?php
use OSC\OM\HTML;
use OSC\OM\OSCOM;
use OSC\OM\Registry;

$OSCOM_Test = Registry::get('Test');
$OSCOM_Page = Registry::get('Site')->getPage();
?>

<div class="row" style="padding-bottom: 30px;">

  <div class="col-sm-6 text-right text-muted">
    <?= $OSCOM_Test->getTitle() . ' v' . $OSCOM_Test->getVersion(); ?>
  </div>
</div>

<?php
if ($OSCOM_MessageStack->exists('Test')) {
    echo $OSCOM_MessageStack->get('Test');
}
?>

template_bottom.php can be blank

main.php is used to display the content when we select Install from the admin menu. Add to that file

<?php
use OSC\OM\HTML;

require(__DIR__ . '/template_top.php');
?>

<div class="row">
  <div class="col-sm-6">
    <div class="panel panel-primary">
      <div class="panel-heading"><?= $OSCOM_Test->getDef('onboarding_intro_title'); ?></div>
      <div class="panel-body">
        <?=
            $OSCOM_Test->getDef('onboarding_intro_body', [
                'button_install' => HTML::button($OSCOM_Test->getDef('button_install'), null, $OSCOM_Test->link(''), null, 'btn-primary')
            ]);
        ?>
      </div>
    </div>
  </div>
</div>

<?php
require(__DIR__ . '/template_bottom.php');
?>

At the moment the button (and page) don't do anything, but we'll work on that next.

 

You should now have a new menu item in the drop down Apps menu in your 2.4 admin which says Test App, with a sub menu saying Install. Clicking that should take you through to the installation page.


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#10   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 15 March 2017 - 22:19

@Harald Ponce de Leon

 


There is also a Content Module hook you can define in your json metadata file. This should automatically load your module - you just need to take care of an administration page if parameters are to be set.

 

 

Hi Harald, could you explain this one in a little more detail?
 

 

In case anyone has missed it, some documentation is up at:

 

https://library.osco...developers

 

 

Will there be additional documentation on Apps? That isn't comprehensive enough :)


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#11   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 16 March 2017 - 00:26

@Dan Cole

 

Re: https://daylerees.co...aces-explained/

 

I read that before I started, helped me get my head around namespaces!


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#12   clustersolutions

clustersolutions
  • Community Sponsor
  • 453 posts
  • Real Name:Tim
  • Gender:Male
  • Location:Los Angeles

Posted 16 March 2017 - 01:10

That's what I am talking about...tribal knowledge! Please keep it coming!

 

@frankl you should actually use a real Vendor and App name for your example instead of Test Test, and make a note that developers must use their own Vendor and App code names (registrations will open for this when the Apps Marketplace goes live).

 

There is also a Content Module hook you can define in your json metadata file. This should automatically load your module - you just need to take care of an administration page if parameters are to be set.

 

In case anyone has missed it, some documentation is up at:

 

https://library.osco...developers



#13   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 16 March 2017 - 06:28

Next we need to set up some action pages so that Home.php actually does something.

Create a new folder OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions

Create a new file, one called Start.php

Start.php will install the app. You can create new database entries and and any database tables needed for the base app through this file.

Actually, Start.php will just start the processing:
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions;

class Start extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $this->page->data['action'] = 'Start';
    }
}

The grunt work is done by Process.php which is in the OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Start folder. Create that now.
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Start;

use OSC\OM\HTTP;
use OSC\OM\OSCOM;
use OSC\OM\Registry;

class Process extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_MessageStack = Registry::get('MessageStack');
        $OSCOM_Test = Registry::get('Test');

        if (!defined('OSCOM_APP_TEST_TEST_STATUS')) {
        $OSCOM_Db = Registry::get('Db');
        $OSCOM_Db->save('configuration', [
        'configuration_title' => 'Enable Related Products App',
        'configuration_key' => 'OSCOM_APP_TEST_TEST_STATUS',
        'configuration_value' => 'True',
        'configuration_description' => 'Should we install optional_related_products ?',
        'configuration_group_id' => '6',
        'sort_order' => '2',
        'set_function' => 'tep_cfg_select_option(array(\'True\', \'False\'), ',
        'date_added' => 'now()'
      ]);
        }

        $OSCOM_Test->redirect('Configure');
    }
}

Right now we are just adding a simple Yes/No is this app installed entry. Later we can use the Process.php file to create database tables and do other tasks if needed.

As you can see, once this file is processed it redirects to the Configure action. The Configure action is for configuring content, payment, shipping, or order total modules which come with the app.

Create Configure.php in OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions (same directory as Start.php)
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions;

use OSC\OM\Registry;

class Configure extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_Test = Registry::get('Test');

        $this->page->setFile('configure.php');
        $this->page->data['action'] = 'Configure';

        $OSCOM_Test->loadDefinitions('admin/configure');

        $modules = $OSCOM_Test->getConfigModules();

        $default_module = 'APPTEST';

        foreach ($modules as $m) {
            if ($OSCOM_Test->getConfigModuleInfo($m, 'is_installed') === true ) {
                $default_module = $m;
                break;
            }
        }

        $this->page->data['current_module'] = (isset($_GET['module']) && in_array($_GET['module'], $modules)) ? $_GET['module'] : $default_module;

    }
}


Once again this file doesn't do much, the grunt work will be done in OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Configure/Process.php, although as you can see it grabs a list of modules (which reside in the OSC/Apps/Test/Test/Module folder, under either Content, Hooks, Shipping, Payment etc subfolders) and checks to see if they are installed.

I've added a default module there, APPTEST, I'm not sure if that's necessary but the Paypal app had a default module so I've got one too :)

Here's the OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Configure/Process.php file
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Configure;

use OSC\OM\Registry;

class Process extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_MessageStack = Registry::get('MessageStack');
        $OSCOM_Test = Registry::get('Test');

        $current_module = 'APPTEST';

        $m = Registry::get('TestAdminConfig' . $current_module);

        foreach ($m->getParameters() as $key) {
            $p = strtolower($key);

            if (isset($_POST[$p])) {
                $OSCOM_Test->saveCfgParam($key, $_POST[$p]);
            }
        }

        $OSCOM_MessageStack->add($OSCOM_Test->getDef('alert_cfg_saved_success'), 'success', 'Test');

        $OSCOM_Test->redirect('Configure&module=' . $current_module);
    }
}

See the getParameters function? That's quite important, I'll get back to that.

Before we go any further, we need to add some more language definitions, this time for the TESTAPP we created at the beginning.

The definition file will live in OSC/Apps/Test/Test/languages/english/modules/APPTEST

In that folder, create a new file called APPTEST.txt

Put in it:
 

module_apptest_title = Test
module_apptest_short_title = Test

module_apptest_legacy_admin_app_button = Manage App

module_apptest_introduction = This module will show the related products on the product info page

You also need to create an admin.txt file in OSC/Apps/Test/Test/languages/english to handle some generic language definitions.
 

app_link_info = Info/Help
app_link_privacy = Privacy
button_back = Back
button_cancel = Cancel
button_delete = Delete
button_dialog_delete = Delete &hellip;
button_install = Install
button_install_title = Install {{title}}
button_save = Save
button_uninstall = Uninstall
button_dialog_uninstall = Uninstall &hellip;
button_view = View

We also need a template file for the Configure.php action file

This lives in the same folder as template_top.php, template_bottom.php and main.php -- OSC/Apps/Test/Test/Sites/Admin/Pages/Home/templates

create a new file in this directory called configure.php.
 

<?php
use OSC\OM\HTML;
use OSC\OM\Registry;

$OSCOM_Page = Registry::get('Site')->getPage();

$current_module = $OSCOM_Page->data['current_module'];

$OSCOM_Test_Config = Registry::get('TestAdminConfig' . $current_module);

require(__DIR__ . '/template_top.php');
?>

<ul id="appTestToolbar" class="nav nav-pills" style="padding-bottom: 15px;">

<?php
foreach ($OSCOM_Test->getConfigModules() as $m) {
    if ($OSCOM_Test->getConfigModuleInfo($m, 'is_installed') === true) {
        echo '<li data-module="' . $m . '"><a href="' . $OSCOM_Test->link('Configure&module=' . $m) . '">' . $OSCOM_Test->getConfigModuleInfo($m, 'short_title') . '</a></li>';
    }
}
?>

  <li class="dropdown"><a class="dropdown-toggle" href="#" data-toggle="dropdown">Install <span class="caret"></span></a>
    <ul class="dropdown-menu">

<?php
foreach ($OSCOM_Test->getConfigModules() as $m) {
    if ($OSCOM_Test->getConfigModuleInfo($m, 'is_installed') === false) {
        echo '<li><a href="' . $OSCOM_Test->link('Configure&module=' . $m) . '">' . $OSCOM_Test->getConfigModuleInfo($m, 'title') . '</a></li>';
    }
}
?>

    </ul>
  </li>
</ul>

<script>
if ($('#appTestToolbar li.dropdown ul.dropdown-menu li').length === 0) {
  $('#appTestToolbar li.dropdown').hide();
}

$(function() {
  var active = '<?= ($OSCOM_Test->getConfigModuleInfo($current_module, 'is_installed') === true) ? $current_module : 'new'; ?>';

  if (active !== 'new') {
    $('#appTestToolbar li[data-module="' + active + '"]').addClass('active');
  } else {
    $('#appTestToolbar li.dropdown').addClass('active');
  }
});
</script>

<?php
if ($OSCOM_Test_Config->is_installed === true) {
    foreach ($OSCOM_Test_Config->req_notes as $rn) {
        echo '<div class="alert alert-warning"><p>' . $rn . '</p></div>';
    }
?>

<form name="paypalConfigure" action="<?= $OSCOM_Test->link('Configure&Process&module=' . $current_module); ?>" method="post">

<div class="panel panel-info oscom-panel">
  <div class="panel-heading">
    <?= $OSCOM_Test->getConfigModuleInfo($current_module, 'title'); ?>
  </div>

  <div class="panel-body">
    <div class="container-fluid">

<?php
    foreach ($OSCOM_Test_Config->getInputParameters() as $cfg) {
        echo $cfg;
    }
?>

    </div>
  </div>
</div>

<p>

<?php
    echo HTML::button($OSCOM_Test->getDef('button_save'), null, null, null, 'btn-success');

    if ($OSCOM_Test->getConfigModuleInfo($current_module, 'is_uninstallable') === true) {
        echo '<span class="pull-right">' . HTML::button($OSCOM_Test->getDef('button_dialog_uninstall'), null, '#', ['params' => 'data-toggle="modal" data-target="#ppUninstallModal"'], 'btn-warning') . '</span>';
    }
?>

</p>

</form>

<?php
    if ($OSCOM_Test->getConfigModuleInfo($current_module, 'is_uninstallable') === true) {
?>

<div id="ppUninstallModal" class="modal" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title"><?= $OSCOM_Test->getDef('dialog_uninstall_title'); ?></h4>
      </div>
      <div class="modal-body">
        <?= $OSCOM_Test->getDef('dialog_uninstall_body'); ?>
      </div>
      <div class="modal-footer">
        <?= HTML::button($OSCOM_Test->getDef('button_uninstall'), null, $OSCOM_Test->link('Configure&Uninstall&module=' . $current_module), null, 'btn-danger'); ?>
        <?= HTML::button($OSCOM_Test->getDef('button_cancel'), null, '#', ['params' => 'data-dismiss="modal"'], 'btn-link'); ?>
      </div>
    </div>
  </div>
</div>

<?php
    }
} else {
?>

<div class="panel panel-warning">
  <div class="panel-heading">
    <?= $OSCOM_Test->getConfigModuleInfo($current_module, 'title'); ?>
  </div>

  <div class="panel-body">
    <?= $OSCOM_Test->getConfigModuleInfo($current_module, 'introduction'); ?>
  </div>
</div>

<p>
  <?= HTML::button($OSCOM_Test->getDef('button_install_title', ['title' => $OSCOM_Test->getConfigModuleInfo($current_module, 'title')]), null, $OSCOM_Test->link('Configure&Install&module=' . $current_module), null, 'btn-warning'); ?>
</p>

<?php
}

require(__DIR__ . '/template_bottom.php');
?>

We've introduced a couple of extra functions in there which will need to be part of the main app, so open OSC/Apps/Test/Test/Test.php and add the following before the closing } bracket:
 

    public function getConfigModules()
    {
        static $result;

        if (!isset($result)) {
            $result = [];

            $directory = OSCOM::BASE_DIR . 'Apps/Test/Test/Module/Admin/Config';

            if ($dir = new \DirectoryIterator($directory)) {
                foreach ($dir as $file) {
                    if (!$file->isDot() && $file->isDir() && is_file($file->getPathname() . '/' . $file->getFilename() . '.php')) {
                        $class = 'OSC\Apps\Test\Test\Module\Admin\Config\\' . $file->getFilename() . '\\' . $file->getFilename();

                        if (is_subclass_of($class, 'OSC\Apps\Test\Test\Module\Admin\Config\ConfigAbstract')) {
                            $sort_order = $this->getConfigModuleInfo($file->getFilename(), 'sort_order');

                            if ($sort_order > 0) {
                                $counter = $sort_order;
                            } else {
                                $counter = count($result);
                            }

                            while (true) {
                                if (isset($result[$counter])) {
                                    $counter++;

                                    continue;
                                }

                                $result[$counter] = $file->getFilename();

                                break;
                            }
                        } else {
                            trigger_error('OSC\Apps\Test\Test\Test::getConfigModules(): OSC\Apps\Test\Test\Module\Admin\Config\\' . $file->getFilename() . '\\' . $file->getFilename() . ' is not a subclass of OSC\Apps\Test\Test\Module\Admin\Config\ConfigAbstract and cannot be loaded.');
                        }
                    }
                }

                ksort($result, SORT_NUMERIC);
            }
        }

        return $result;
    }
    
    public function getConfigModuleInfo($module, $info)
    {
        if (!Registry::exists('TestAdminConfig' . $module)) {
            $class = 'OSC\Apps\Test\Test\Module\Admin\Config\\' . $module . '\\' . $module;

            Registry::set('TestAdminConfig' . $module, new $class);
        }

        return Registry::get('TestAdminConfig' . $module)->$info;
    }


Now change the action of the button in OSC/Apps/Test/Test/Sites/Admin/Pages/Home/templates/main.php from
 

'button_install' => HTML::button($OSCOM_Test->getDef('button_install'), null, $OSCOM_Test->link(''), null, 'btn-primary')

to
 

'button_install' => HTML::button($OSCOM_Test->getDef('button_install'), null, $OSCOM_Test->link('Start&Process'), null, 'btn-primary')

To install modules, we need to add a new file in the Configure actions folder OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Configure

Create a new file in this folder called Install.php
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Configure;

use OSC\OM\Registry;

class Install extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_MessageStack = Registry::get('MessageStack');
        $OSCOM_Test = Registry::get('Test');

        $current_module = $this->page->data['current_module'];

        $m = Registry::get('TestAdminConfig' . $current_module);
        $m->install();

        $OSCOM_MessageStack->add($OSCOM_Test->getDef('alert_module_install_success'), 'success', 'Test');

        $OSCOM_Test->redirect('Configure&module=' . $current_module);
    }
}


Now we need to organise the configuration classes so that it will configure any modules the app may have.

Create a new folder OSC/Apps/Test/Test/Module/Admin/Config and create two new files, ConfigAbstract.php and ConfigParamAbstract.php

In the ConfigAbstract.php file, add this

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Module\Admin\Config;

use OSC\OM\OSCOM;
use OSC\OM\Registry;

abstract class ConfigAbstract
{
    protected $app;

    public $code;
    public $title;
    public $short_title;
    public $introduction;
    public $req_notes = [];
    public $is_installed = false;
    public $is_uninstallable = false;
    public $is_migratable = false;
    public $sort_order = 0;

    abstract protected function init();

    final public function __construct()
    {
        $this->app = Registry::get('Test');

        $this->code = (new \ReflectionClass($this))->getShortName();

        $this->app->loadDefinitions('modules/' . $this->code . '/' . $this->code);

        $this->init();
    }

    public function install()
    {
        $cut_length = strlen('OSCOM_APP_TEST_' . $this->code . '_');

        foreach ($this->getParameters() as $key) {
            $p = strtolower(substr($key, $cut_length));

            $class = 'OSC\Apps\Test\Test\Module\Admin\Config\\' . $this->code . '\Params\\' . $p;

            $cfg = new $class($this->code);

            $this->app->saveCfgParam($key, $cfg->default, isset($cfg->title) ? $cfg->title : null, isset($cfg->description) ? $cfg->description : null, isset($cfg->set_func) ? $cfg->set_func : null);
        }
    }

    public function uninstall()
    {
        $Qdelete = $this->app->db->prepare('delete from :table_configuration where configuration_key like :configuration_key');
        $Qdelete->bindValue(':configuration_key', 'OSCOM_APP_TEST_' . $this->code . '_%');
        $Qdelete->execute();

        return $Qdelete->rowCount();
    }

    public function getParameters()
    {
        $result = [];

        $directory = OSCOM::BASE_DIR . 'Apps/Test/Test/Module/Admin/Config/' . $this->code . '/Params';

        if ($dir = new \DirectoryIterator($directory)) {
            foreach ($dir as $file) {
                if (!$file->isDot() && !$file->isDir() && ($file->getExtension() == 'php')) {
                    $class = 'OSC\Apps\Test\Test\Module\Admin\Config\\' . $this->code . '\\Params\\' . $file->getBasename('.php');

                    if (is_subclass_of($class, 'OSC\Apps\Test\Test\Module\Admin\Config\ConfigParamAbstract')) {
                        if ($this->code == 'G') {
                            $result[] = 'OSCOM_APP_TEST_' . strtoupper($file->getBasename('.php'));
                        } else {
                            $result[] = 'OSCOM_APP_TEST_' . $this->code . '_' . strtoupper($file->getBasename('.php'));
                        }
                    } else {
                        trigger_error('OSC\Apps\Test\Test\Module\Admin\Config\\ConfigAbstract::getParameters(): OSC\Apps\Test\Test\Module\Admin\Config\\' . $this->code . '\\Params\\' . $file->getBasename('.php') . ' is not a subclass of OSC\Apps\Test\Test\Module\Admin\Config\ConfigParamAbstract and cannot be loaded.');
                    }
                }
            }
        }

        return $result;
    }

    public function getInputParameters()
    {
        $result = [];

        if ($this->code == 'G') {
            $cut = 'OSCOM_APP_TEST_';
        } else {
            $cut = 'OSCOM_APP_TEST_' . $this->code . '_';
        }

        $cut_length = strlen($cut);

        foreach ($this->getParameters() as $key) {
            $p = strtolower(substr($key, $cut_length));

            $class = 'OSC\Apps\Test\Test\Module\Admin\Config\\' . $this->code . '\Params\\' . $p;

            $cfg = new $class($this->code);

            if (!defined($key)) {
              $this->app->saveCfgParam($key, $cfg->default, isset($cfg->title) ? $cfg->title : null, isset($cfg->description) ? $cfg->description : null, isset($cfg->set_func) ? $cfg->set_func : null);
            }

            if ($cfg->app_configured !== false) {
                if (is_numeric($cfg->sort_order)) {
                    $counter = (int)$cfg->sort_order;
                } else {
                    $counter = count($result);
                }

                while (true) {
                    if (isset($result[$counter])) {
                        $counter++;

                        continue;
                    }

                    $set_field = $cfg->getSetField();

                    if (!empty($set_field)) {
                        $result[$counter] = $set_field;
                    }

                    break;
                }
            }
        }

        ksort($result, SORT_NUMERIC);

        return $result;
    }
}

In the ConfigParamAbstract.php file, add this:
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Module\Admin\Config;

use OSC\OM\Registry;

abstract class ConfigParamAbstract extends \OSC\Sites\Admin\ConfigParamAbstract
{
    protected $app;
    protected $config_module;

    protected $key_prefix = 'oscom_app_Test_';
    public $app_configured = true;

    public function __construct($config_module)
    {
        $this->app = Registry::get('Test');

        if ($config_module != 'G') {
            $this->key_prefix .= strtolower($config_module) . '_';
        }

        $this->config_module = $config_module;

        $this->code = (new \ReflectionClass($this))->getShortName();

        $this->app->loadDefinitions('modules/' . $config_module . '/Params/' . $this->code);

        parent::__construct();
    }
}


Now create a new folder (you need one for each of your modules if you have more than one) so that your module can be configured -- in this case OSC/Apps/Test/Test/Module/Admin/Config/APPTEST
and in that folder create a file APPTEST.php
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Module\Admin\Config\APPTEST;

use OSC\OM\OSCOM;

class APPTEST extends \OSC\Apps\Test\Test\Module\Admin\Config\ConfigAbstract
{
    protected $_cm_code = 'product_info/cm_pi_related_products';

    public $is_uninstallable = true;
    public $is_migratable = true;
    public $sort_order = 1000;

    protected function init()
    {
        $this->title = $this->app->getDef('module_apptest_title');
        $this->short_title = $this->app->getDef('module_apptest_short_title');
        $this->introduction = $this->app->getDef('module_apptest_introduction');

        $this->is_installed = defined('OSCOM_APP_TEST_APPTEST_STATUS') && (trim(OSCOM_APP_TEST_APPTEST_STATUS) != '');

    }

    public function install()
    {
        parent::install();

        $installed = explode(';', MODULE_CONTENT_INSTALLED);
        $installed[] = 'product_info/' . $this->app->vendor . '\\' . $this->app->code . '\\' . $this->code;

        $this->app->saveCfgParam('MODULE_CONTENT_INSTALLED', implode(';', $installed));
    }

    public function uninstall()
    {
        parent::uninstall();

        $installed = explode(';', MODULE_CONTENT_INSTALLED);
        $installed_pos = array_search('product_info/' . $this->app->vendor . '\\' . $this->app->code . '\\' . $this->code, $installed);

        if ($installed_pos !== false) {
            unset($installed[$installed_pos]);

            $this->app->saveCfgParam('MODULE_CONTENT_INSTALLED', implode(';', $installed));
        }
    }
}


Very importantly, we need to add parameters. You may recognise these from the old modules, something like
 

tep_db_query("insert into " . TABLE_CONFIGURATION . " (configuration_title, configuration_key, configuration_value, configuration_description, configuration_group_id, sort_order, set_function, date_added) values ('Enable Account Footer Module', 'MODULE_CONTENT_FOOTER_ACCOUNT_STATUS', 'True', 'Do you want to enable the Account content module?', '6', '1', 'tep_cfg_select_option(array(\'True\', \'False\'), ', now())");

 


For apps, things are completely different. There is a special Params folder with one file per parameter for each module.
Right now I'll just make one parameter, status, for our content module, that's just to turn it on or off. We will add new parameters later.

Create a new directory OSC/Apps/Test/Test/Module/Admin/Config/APPTEST/Params and then create a new file in there called status.php

(Note: The class name has to be identical to the filename)

Here are the contents:
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Module\Admin\Config\APPTEST\Params;

use OSC\OM\HTML;

class status extends \OSC\Apps\Test\Test\Module\Admin\Config\ConfigParamAbstract
{
    public $default = '1';
    public $sort_order = 100;

    protected function init()
    {
        $this->title = $this->app->getDef('cfg_apptest_status_title');
        $this->description = $this->app->getDef('cfg_apptest_status_desc');
    }

    public function getInputField()
    {
        $value = $this->getInputValue();

        $input = '<div class="btn-group" data-toggle="buttons">' .
                 '  <label class="btn btn-info' . ($value == '1' ? ' active' : '') . '">' . HTML::radioField($this->key, '1', ($value == '1')) . $this->app->getDef('cfg_apptest_status_true') . '</label>' .
                 '  <label class="btn btn-info' . ($value == '-1' ? ' active' : '') . '">' . HTML::radioField($this->key, '-1', ($value == '-1')) . $this->app->getDef('cfg_apptest_status_disabled') . '</label>' .
                 '</div>';

        return $input;
    }
}


You will also need to create a corresponding language definition text file for each parameter in OSC/Apps/Test/Test/languages/english/modules/APPTEST/Params, in this case status.txt
 

cfg_apptest_status_title = Status
cfg_apptest_status_desc = Set this to Enabled to use the Product Info Page module.

cfg_apptest_status_true = Enabled
cfg_apptest_status_disabled = Disabled


We don't want the Install option to be available on the app menu once we have actually installed the app, so let's make it change to Configure so we can configure the module/s

Open OSC/Apps/Test/Test/Module/Admin/Menu/Test.php and change

 

$test_menu = [
            [
                'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                'title' => $OSCOM_Test->getDef('module_admin_menu_install'),
                'link' => $OSCOM_Test->link()
            ]
        ];

        return array('heading' => $OSCOM_Test->getDef('module_admin_menu_title'),
                     'apps' => $test_menu);

                    
to

     

  $test_menu_check = [
            'OSCOM_APP_TEST_TEST_STATUS'
        ];

        foreach ($test_menu_check as $value) {
            if (defined($value) && !empty(constant($value))) {
                $test_menu = [
                    [
                        'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                        'title' => $OSCOM_Test->getDef('module_admin_menu_configure'),
                        'link' => $OSCOM_Test->link('Configure')
                    ]
                ];

                break;
            }
        }

        return array('heading' => $OSCOM_Test->getDef('module_admin_menu_title'),
                     'apps' => $test_menu);

                    
And add
 

module_admin_menu_configure = Configure


to OSC/Apps/Test/Test/languages/english/admin/modules/boxes/test.txt

 

Nearly there!

I've modified the content module file so that it takes advantage of a few features, so open up OSC/Apps/Test/Test/Module/Content/APPTEST.php we created in the first post and replace the contents with this

 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

  namespace OSC\Apps\Test\Test\Module\Content;

  use OSC\OM\HTML;
  use OSC\OM\OSCOM;
  use OSC\OM\Registry;
  use OSC\Apps\Test\Test\Test as TestApp;

  class APPTEST implements \OSC\OM\Modules\ContentInterface {
    public $code, $group, $title, $description, $sort_order, $enabled, $app;

    function __construct() {
      if (!Registry::exists('Test')) {
        Registry::set('Test', new TestApp());
      }

      $this->app = Registry::get('Test');
      $this->app->loadDefinitions('modules/APPTEST/APPTEST');

      $this->code = 'APPTEST';
      $this->group = 'product_info';

      $this->title = 'Test App';
      $this->description = '<div align="center">' . HTML::button($this->app->getDef('module_login_legacy_admin_app_button'), null, $this->app->link('Configure&module=APPTEST'), null, 'btn-primary') . '</div>';
      $this->sort_order = '10';
      
          if ( OSCOM_APP_TEST_APPTEST_STATUS < '1' ) {

            $this->enabled = false;
          } else {
            $this->enabled = true;
          }
    }

    function execute() {
      global $oscTemplate;


      ob_start();
      include(__DIR__ . '/templates/APPTEST.php');
      $template = ob_get_clean();

      $oscTemplate->addContent($template, $this->group);
    }
    
    function isEnabled() {
      return $this->enabled;
    }

    function check() {
      return defined('OSCOM_APP_TEST_APPTEST_STATUS');
    }

    function install() {
      $this->app->redirect('Configure&Install&module=APPTEST');
    }

    function remove() {
      $this->app->redirect('Configure&Uninstall&module=APPTEST');
    }

    function keys() {
      return array('OSCOM_APP_TEST_APPTEST_STATUS');
    }
  }
?>


Also, we need to be able to uninstall modules so create a new file in the Configure actions folder OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Configure called (surprise!) Uninstall.php
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Configure;

use OSC\OM\Registry;

class Uninstall extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_MessageStack = Registry::get('MessageStack');
        $OSCOM_Test = Registry::get('Test');

        $current_module = $this->page->data['current_module'];

        $m = Registry::get('TestAdminConfig' . $current_module);
        $m->uninstall();

        $OSCOM_MessageStack->add($OSCOM_Test->getDef('alert_module_uninstall_success'), 'success', 'Test');

        $OSCOM_Test->redirect('Configure&module=' . $current_module);
    }
}

A couple more language definitions and we're done for the day.

Create a new file configure.txt in OSC/Apps/Test/Test/languages/english/admin
 

section_general = General
section_more = +

dialog_uninstall_title = Uninstall Module?
dialog_uninstall_body = Are you sure you want to uninstall this module?

alert_module_install_success = Module has been successfully installed.
alert_module_uninstall_success = Module has been successfully uninstalled.
alert_cfg_saved_success = Configuration parameters have been successfully saved.


Now you have a functioning app that consists of a static content module. All you can do is turn the module on and off, but the groundwork has been laid for the rest of the app to come to life. You can also use this simple app as a base to experiment and create your own. Next I'll be adding the ability to uninstall the app.

 

Also, once you've installed the content module and it's enabled, then go to Legacy -> Modules -> Content and select Test App, you'll see a Manage App button just like the Paypal App's modules.
 


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#14   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 16 March 2017 - 07:18

This section is incorrect

 

Open OSC/Apps/Test/Test/Module/Admin/Menu/Test.php and change

 

$test_menu = [
            [
                'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                'title' => $OSCOM_Test->getDef('module_admin_menu_install'),
                'link' => $OSCOM_Test->link()
            ]
        ];

        return array('heading' => $OSCOM_Test->getDef('module_admin_menu_title'),
                     'apps' => $test_menu);
                    

to

     

 

$test_menu_check = [
            'OSCOM_APP_TEST_TEST_STATUS'
        ];

        foreach ($test_menu_check as $value) {
            if (defined($value) && !empty(constant($value))) {
                $test_menu = [
                    [
                        'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                        'title' => $OSCOM_Test->getDef('module_admin_menu_configure'),
                        'link' => $OSCOM_Test->link('Configure')
                    ]
                ];

                break;
            }
        }

        return array('heading' => $OSCOM_Test->getDef('module_admin_menu_title'),
                     'apps' => $test_menu);

SHOULD BE:

 

Open OSC/Apps/Test/Test/Module/Admin/Menu/Test.php and change

$test_menu = [
            [
                'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                'title' => $OSCOM_Test->getDef('module_admin_menu_install'),
                'link' => $OSCOM_Test->link()
            ]
        ];

        return array('heading' => $OSCOM_Test->getDef('module_admin_menu_title'),
                     'apps' => $test_menu);

                    
to

     $test_menu = [            [
                'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                'title' => $OSCOM_Test->getDef('module_admin_menu_install'),
                'link' => $OSCOM_Test->link()
            ]
        ]; 

        $test_menu_check = [
            'OSCOM_APP_TEST_TEST_STATUS'
        ];

        foreach ($test_menu_check as $value) {
            if (defined($value) && !empty(constant($value))) {
                $test_menu = [
                    [
                        'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                        'title' => $OSCOM_Test->getDef('module_admin_menu_configure'),
                        'link' => $OSCOM_Test->link('Configure')
                    ]
                ];

                break;
            }
        }

        return array('heading' => $OSCOM_Test->getDef('module_admin_menu_title'),
                     'apps' => $test_menu);

Sorry about that!


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#15   burt

burt

    I drink and I know things

  • Community Team
  • 12,463 posts
  • Real Name:G Burton
  • Gender:Male
  • Location:UK/DEV/on

Posted 16 March 2017 - 10:15

Topic Pinned.

I am following this closely, thank you @frankl

This is a signature that appears on all my posts.  It is not specifically aimed at you.

 

IF YOU MAKE A POST REQUESTING HELP...please state the exact version of osCommerce that you are using. THANKS
 
If you are still on the old style osCommerce, it is time to move to Responsive.

 


#16   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 16 March 2017 - 10:32

@burt

 

Thanks Gary. Hopefully some people find it useful, I find the concept of Apps really exciting. Guess that makes me an osCommerce Nerd lol.


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#17   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 16 March 2017 - 10:34

Oops, another error!

 

in OSC/Apps/Test/Test/Module/Content/APPTEST.php change this line 

$this->description = '<div align="center">' . HTML::button($this->app->getDef('module_login_legacy_admin_app_button'), null, $this->app->link('Configure&module=APPTEST'), null, 'btn-primary') . '</div>';

to

$this->description = '<div align="center">' . HTML::button($this->app->getDef('module_apptest_legacy_admin_app_button'), null, $this->app->link('Configure&module=APPTEST'), null, 'btn-primary') . '</div>';

Forgot to update the language def

      


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#18   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 20 March 2017 - 20:54

I'll be posting more of my tutorial soon, stay posted!


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#19   frankl

frankl

    One of the originals...

  • Community Sponsor
  • 475 posts
  • Real Name:Frank
  • Gender:Male
  • Location:Sydney, Australia

Posted 21 March 2017 - 05:04

Back to it. I hope it hasn't been too confusing so far.

I've changed my mind about doing the uninstall next, instead I'm going to show what to do with the admin page.

First of all, we need to create the table and some configuration options which, if you've been following along and creating the app with me, requires a reinstall. Luckily, this is easy as.

Use phpMyAdmin or similar to delete any items with OSCOM_APP_TEST_ from the configuration table.

That's it.

 

Before we can reinstall we need to modify and add some files.

Let's add the required items to the install routine by opening OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Start/Process.php and replacing the contents with this:
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Start;

use OSC\OM\HTTP;
use OSC\OM\OSCOM;
use OSC\OM\Registry;

class Process extends \OSC\OM\PagesActionsAbstract
{
    
    public function execute()
    {
        $OSCOM_MessageStack = Registry::get('MessageStack');
        $OSCOM_Test = Registry::get('Test');

        if (!defined('OSCOM_APP_TEST_TEST_STATUS')) {
        $OSCOM_Db = Registry::get('Db');
        $OSCOM_Db->save('configuration', [
        'configuration_title' => 'Enable Related Products App',
        'configuration_key' => 'OSCOM_APP_TEST_TEST_STATUS',
        'configuration_value' => 'True',
        'configuration_description' => 'Should we install optional_related_products ?',
        'configuration_group_id' => '6',
        'sort_order' => '2',
        'set_function' => 'tep_cfg_select_option(array(\'True\', \'False\'), ',
        'date_added' => 'now()'
      ]);
        }
        
        $data = [
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL'  => 'True',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME' => 'True',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR' => ' | ',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_ROWS_LIST_OPTIONS' => '20',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH' => '30',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH' => '30',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_CONFIRM_DELETE' => 'True',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_INSERT_AND_INHERIT' => 'False'
            ];

        foreach ($data as $key => $value) {
            $OSCOM_Test->saveCfgParam($key, $value);
        }
        
        $Qcheck = $OSCOM_Db->query('show tables like ":table_products_related_products"');

        if ($Qcheck->fetch() === false) {
            $sql = <<<EOD
CREATE TABLE :table_products_related_products (
  pop_id int(11) NOT NULL auto_increment,
  pop_products_id_master int(11) NOT NULL DEFAULT '0',
  pop_products_id_slave int(11) NOT NULL DEFAULT '0',
  pop_order_id smallint(6) NOT NULL DEFAULT '0',
  PRIMARY KEY (pop_id)
) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
EOD;

            $OSCOM_Db->exec($sql);
        }

        $OSCOM_Test->redirect('Configure');
    }
}

As you can see we've added a database table and some admin options in there so they can be created when we install the app.

We will also need a way to configure those options, so first let's build that page.

Create a new file in OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions called Options.php

Put this in it:
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions;

use OSC\OM\Registry;

class Options extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_Test = Registry::get('Test');

        $this->page->setFile('options.php');
        $this->page->data['action'] = 'Options';

        $OSCOM_Test->loadDefinitions('admin/options');

        $data = [
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL',
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME',
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR',
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_ROWS_LIST_OPTIONS',
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH',
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH',
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_CONFIRM_DELETE',
            'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_INSERT_AND_INHERIT'
        ];

        foreach ($data as $key) {
            if (!defined($key)) {
                $OSCOM_Test->saveCfgParam($key, '');
            }
        }
    }
}


Now create a corresponding folder OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Options, and create a new file in that folder called Process.php

This file will process any changes we make to the options.

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Options;

use OSC\OM\HTML;
use OSC\OM\Registry;

class Process extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_MessageStack = Registry::get('MessageStack');
        $OSCOM_Test = Registry::get('Test');

        $data = [];

             $data = [
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL'  => isset($_POST['use_model']) ? HTML::sanitize($_POST['use_model']) : '',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME' => isset($_POST['use_name']) ? HTML::sanitize($_POST['use_name']) : '',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR' => isset($_POST['model_separator']) ? HTML::sanitize($_POST['model_separator']) : '',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_ROWS_LIST_OPTIONS' => isset($_POST['max_rows_list_options']) ? HTML::sanitize($_POST['max_rows_list_options']) : '',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH' => isset($_POST['max_name_length']) ? HTML::sanitize($_POST['max_name_length']) : '',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH' => isset($_POST['max_display_length']) ? HTML::sanitize($_POST['max_display_length']) : '',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_CONFIRM_DELETE' => isset($_POST['confirm_delete']) ? HTML::sanitize($_POST['confirm_delete']) : '',
                'OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_INSERT_AND_INHERIT' => isset($_POST['insert_and_inherit']) ? HTML::sanitize($_POST['insert_and_inherit']) : ''
            ];

        foreach ($data as $key => $value) {
            $OSCOM_Test->saveCfgParam($key, $value);
        }

        $OSCOM_MessageStack->add($OSCOM_Test->getDef('alert_credentials_saved_success'), 'success', 'Test');

        $OSCOM_Test->redirect('Admin');
    }
}


Then create the template file, options.php, in the OSC/Apps/Test/Test/Sites/Admin/Pages/Home/templates folder.

That file's contents are:
 

<?php
use OSC\OM\HTML;
use OSC\OM\Registry;

$OSCOM_Page = Registry::get('Site')->getPage();
require(__DIR__ . '/template_top.php');
?>

<form name="TestOptions" action="<?= $OSCOM_Test->link('Options&Process'); ?>" method="post">

<div class="panel panel-warning">
  <div class="panel-heading">
    <?= $OSCOM_Test->getDef('app_test_options_title'); ?>
  </div>

  <div class="panel-body">
    <div class="row">
      <div class="col-sm-6">
        <div class="btn-group" data-toggle="buttons">
          <label for="use_model"><?= $OSCOM_Test->getDef('app_test_options_use_model'); ?></label><br>
          <?php echo '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True' ? ' active' : '') . '">' . HTML::radioField('use_model', 'True', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True')) . $OSCOM_Test->getDef('app_test_options_true') . '</label>' .
          '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'False' ? ' active' : '') . '">' . HTML::radioField('use_model', 'False', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'False')) . $OSCOM_Test->getDef('app_test_options_false') . '</label>';
          ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_use_model_desc'); ?></div>

        <div class="btn-group" data-toggle="buttons">
          <label for="use_name"><?= $OSCOM_Test->getDef('app_test_options_use_name'); ?></label><br>
          <?php echo '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True' ? ' active' : '') . '">' . HTML::radioField('use_name', 'True', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True')) . $OSCOM_Test->getDef('app_test_options_true') . '</label>' .
          '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'False' ? ' active' : '') . '">' . HTML::radioField('use_name', 'False', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'False')) . $OSCOM_Test->getDef('app_test_options_false') . '</label>';
          ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_use_name_desc'); ?></div>

        <div class="form-group">
          <label for="model_separator"><?= $OSCOM_Test->getDef('app_test_options_model_separator'); ?></label>
          <?php echo HTML::inputField('model_separator', OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR, 'id="model_separator"'); ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_model_separator_desc'); ?></div>
        
        <div class="btn-group" data-toggle="buttons">
          <label for="use_model"><?= $OSCOM_Test->getDef('app_test_options_confirm_delete'); ?></label><br>
          <?php echo '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_CONFIRM_DELETE == 'True' ? ' active' : '') . '">' . HTML::radioField('confirm_delete', 'True', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_CONFIRM_DELETE == 'True')) . $OSCOM_Test->getDef('app_test_options_true') . '</label>' .
          '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_CONFIRM_DELETE == 'False' ? ' active' : '') . '">' . HTML::radioField('confirm_delete', 'False', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_CONFIRM_DELETE == 'false')) . $OSCOM_Test->getDef('app_test_options_false') . '</label>';
          ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_confirm_delete_desc'); ?></div>
      </div>

      <div class="col-sm-6">
        <div class="form-group">
          <label for="max_rows_list_options"><?= $OSCOM_Test->getDef('app_test_options_max_rows_list_options'); ?></label>
          <?php echo HTML::inputField('max_rows_list_options', OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_ROWS_LIST_OPTIONS, 'id="max_rows_list_options"'); ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_max_rows_list_options_desc'); ?></div>

        <div class="form-group">
          <label for="max_name_length"><?= $OSCOM_Test->getDef('app_test_options_max_name_length'); ?></label>
          <?php echo HTML::inputField('max_name_length', OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH, 'id="max_name_length"'); ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_max_name_length_desc'); ?></div>

        <div class="form-group">
          <label for="max_display_length"><?= $OSCOM_Test->getDef('app_test_options_max_display_length'); ?></label>
          <?php echo HTML::inputField('max_display_length', OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH, 'id="max_display_length"'); ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_max_display_length_desc'); ?></div>
        <div class="btn-group" data-toggle="buttons">
          <label for="insert_and_inherit"><?= $OSCOM_Test->getDef('app_test_options_insert_and_inherit'); ?></label><br>
          <?php echo '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_INSERT_AND_INHERIT == 'True' ? ' active' : '') . '">' . HTML::radioField('insert_and_inherit', 'True', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_INSERT_AND_INHERIT == 'True')) . $OSCOM_Test->getDef('app_test_options_true') . '</label>' .
          '  <label class="btn btn-info' . (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_INSERT_AND_INHERIT == 'False' ? ' active' : '') . '">' . HTML::radioField('insert_and_inherit', 'False', (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_INSERT_AND_INHERIT == 'False')) . $OSCOM_Test->getDef('app_test_options_false') . '</label>';
          ?>
        </div>
        <div class="help-block"><?= $OSCOM_Test->getDef('app_test_options_insert_and_inherit_desc'); ?></div>
      </div>
    </div>
  </div>
</div>

<p><?= HTML::button($OSCOM_Test->getDef('button_save'), null, null, null, 'btn-success'); ?></p>

</form>

<?php
require(__DIR__ . '/template_bottom.php');
?>


And lastly, we need the language definitions file in OSC/Apps/Test/Test/languages/english/admin, options.txt

Contents:
 

app_test_options_title = Related Products Admin Options
app_test_options_true = True
app_test_options_false = False
app_test_options_use_model = Use Model?
app_test_options_use_model_desc = Use product model in lists. When product name or Id is also selected, product ID is displayed first.
app_test_options_use_name = Use Name?
app_test_options_use_name_desc = Use product name in lists. When product model or ID is also selected, product ID or model is displayed first.
app_test_options_model_separator = Separator
app_test_options_model_separator_desc = Enter the characters you would like to separate ID, model and name, when using 2 or 3. Leave empty if only using one.
app_test_options_confirm_delete = Confirm delete?
app_test_options_confirm_delete_desc = When set to True, a confirmation box will pop-up when deleting an association. Set to False to delete without confirmation.
app_test_options_insert_and_inherit = Combine Insert with Inherit
app_test_options_insert_and_inherit_desc = When set to True, clicking on Inherit will also Insert the product association. When False, Inherit works as before.
app_test_options_max_rows_list_options = Maximum Rows
app_test_options_max_rows_list_options_desc = Sets the maximum number of rows to display per page.
app_test_options_max_name_length = Drop-Down List Maximum Length
app_test_options_max_name_length_desc = Sets the maximum length (in characters) of product name displayed in drop-down lists. Enter 0 to set this option to false.
app_test_options_max_display_length = Display List Maximum Length
app_test_options_max_display_length_desc = Sets the maximum length (in characters) of product name displayed in list. Enter 0 to set this option to false.
alert_credentials_saved_success = Configuration options saved


The Options page is now complete.

 

We won't do a menu item for the options, we'll have a button from the main admin page (which we are about to create) to reach the options configuration page.

We will need to create a menu item for the main administration page though, so open up OSC/Apps/Test/Test/Module/Admin/Menu/Test.php

Change
       

foreach ($test_menu_check as $value) {
            if (defined($value) && !empty(constant($value))) {
                $test_menu = [
                    [
                        'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                        'title' => $OSCOM_Test->getDef('module_admin_menu_configure'),
                        'link' => $OSCOM_Test->link('Configure')
                    ]
                    
                ];

                break;
            }
        }

To

 

     foreach ($test_menu_check as $value) {
            if (defined($value) && !empty(constant($value))) {
                $test_menu = [
                    [
                        'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                        'title' => $OSCOM_Test->getDef('module_admin_menu_admin'),
                        'link' => $OSCOM_Test->link('Admin')
                    ],
                    [
                        'code' => $OSCOM_Test->getVendor() . '\\' . $OSCOM_Test->getCode(),
                        'title' => $OSCOM_Test->getDef('module_admin_menu_configure'),
                        'link' => $OSCOM_Test->link('Configure')
                    ]
                    
                ];

                break;
            }
        }

        
We also need the language definition for that menu item, so open up OSC/Apps/Test/Test/languages/english/admin/modules/boxes/test.txt and make sure you have:
 

module_admin_menu_title = Test App
module_admin_menu_install = Install
module_admin_menu_configure = Configure
module_admin_menu_admin = Admin


Now for the admin page itself.

Create a new file in OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions called Admin.php

Once again, in the same process as when we created the Options page, we also need to create a corresponding folder in the Action folder containing action file/s; a template; and a language definitions file.

Put the following content in OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Admin.php
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions;

use OSC\OM\Registry;

class Admin extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_PayPal = Registry::get('Test');

        $this->page->setFile('admin.php');
        $this->page->data['action'] = 'Admin';

        $OSCOM_PayPal->loadDefinitions('admin/admin');
    }
}


Create the new folder OSC/Apps/Test/Test/Sites/Admin/Pages/Home/Actions/Admin and in that folder create 2 files. Why 2? Because we want to take actions on the Admin page that need to be processed using different namespaces.

The first file is Admin.php. Put this in it:
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Admin;

use OSC\OM\Registry;

class Admin extends \OSC\OM\PagesActionsAbstract
{

    
        public function execute()
    {
            
                $this->page->setFile('admin.php');

        }

}


The second file is Insert.php. We are going to use that to insert rows in the related products table. Copy this into it:
 

<?php
/**
  * Test App for osCommerce Online Merchant
  *
  * @copyright (c) 2016 osCommerce; https://www.oscommerce.com
  * @licensed2kill MIT; https://www.oscommerce.com/license/mit.txt
  */

namespace OSC\Apps\Test\Test\Sites\Admin\Pages\Home\Actions\Admin;

use OSC\OM\Registry;
use OSC\OM\HTML;

class Insert extends \OSC\OM\PagesActionsAbstract
{
    public function execute()
    {
        $OSCOM_MessageStack = Registry::get('MessageStack');
        $OSCOM_Test = Registry::get('Test');
        $OSCOM_Db = Registry::get('Db');


                $products_id_master = isset($_POST['products_id_master']) ? HTML::sanitize($_POST['products_id_master']) : '';
                $products_id_slave = isset($_POST['products_id_slave']) ? HTML::sanitize($_POST['products_id_slave']) : '';
                $pop_order_id = isset($_POST['pop_order_id']) ? HTML::sanitize($_POST['pop_order_id']) : '';


        if ($products_id_master != $products_id_slave) {
            
            $Qcheck = $OSCOM_Db->prepare('select pop_id from :table_products_related_products where pop_products_id_master = :products_id_master and pop_products_id_slave = :products_id_slave');
            $Qcheck->bindInt(':products_id_master', $products_id_master);
            $Qcheck->bindInt(':products_id_slave', $products_id_slave);
            $Qcheck->execute();
            if ($Qcheck->fetch() === false) {
              $Qinsert = $OSCOM_Db->prepare('insert into :table_products_related_products (pop_products_id_master, pop_products_id_slave, pop_order_id) values (:products_id_master, :products_id_slave, :pop_order_id)');
              $Qinsert->bindInt(':products_id_master', $products_id_master);
              $Qinsert->bindInt(':products_id_slave', $products_id_slave);
              $Qinsert->bindInt(':pop_order_id', $pop_order_id);
              $Qinsert->execute();

                 $OSCOM_MessageStack->add($OSCOM_Test->getDef('alert_product_added_success'), 'success', 'Test');
            } else {
                $OSCOM_MessageStack->add($OSCOM_Test->getDef('alert_product_added_failure'), 'warning', 'Test');
            }
        }

        $OSCOM_Test->redirect('Admin&products_id_master=' . $products_id_master);
    }
}


Of course we need the template file (in this case OSC/Apps/Test/Test/Sites/Admin/Pages/Home/templates/admin.php)

Put this in it:
 

<?php

  use OSC\OM\FileSystem;
  use OSC\OM\HTML;
  use OSC\OM\OSCOM;
  use OSC\OM\Registry;
 
  $OSCOM_Page = Registry::get('Site')->getPage();

  require(__DIR__ . '/template_top.php');

  if (!isset($_GET['page']) || !is_numeric($_GET['page'])) {
    $_GET['page'] = 1;
  }

  $action = (isset($_GET['action']) ? $_GET['action'] : '');
 
  $products_id_slave = '';
  $products_id_master = '';
  $products_id_view = '';
 
  $products_id_view = (isset($_GET['products_id_view']) && $_GET['products_id_view'] != 0) ? (int)$_GET['products_id_view'] : null;
  $products_id_master = (isset($_GET['products_id_master']) && $_GET['products_id_master'] != 0) ? (int)$_GET['products_id_master'] : null;
  if ($products_id_master) {
    $products_id_view = $products_id_master;
  }
 
  if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True' && OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') {
        $MenuOrderBy = ' order by pd.products_name, p.products_model';
  } elseif (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') {
        $MenuOrderBy = ' order by pd.products_name';
  } elseif (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True') {
        $MenuOrderBy = ' order by p.products_model';
  }
 

?>
<div class="text-right">
  <?= HTML::button($OSCOM_Test->getDef('app_test_button_options'), null, $OSCOM_Test->link('Options'), null, 'btn-info'); ?>
</div>
<h2><i class="fa fa-arrows-h"></i> <a href="<?= $OSCOM_Test->link('Admin'); ?>"><?= $OSCOM_Test->getDef('app_test_admin_heading_title'); ?></a></h2>

<?php
    if ( defined('OSCOM_APP_TEST_TEST_STATUS') )
    { // check if product info module installed
?>
<div class="relatedShowAll form-inline col-sm-6">
<?php
            $related_what = "p.products_id";                
              $related_from =  " from :table_products p, :table_products_related_products pa";
            $related_where =  " where pa.pop_products_id_master = p.products_id";
              if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True') {
                  $related_what .= ", p.products_model";
              }
              if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') {
                  $related_what .= ", pd.products_name";
                  $related_from .=  ", :table_products_description pd";
                  $related_where .=  " and p.products_id = pd.products_id";
                  $related_where .=  " and pd.language_id = :languages_id";
            }
            
            $Qrelated = $OSCOM_Test->db->prepare('select distinct ' . $related_what . $related_from . $related_where . $MenuOrderBy);
            $Qrelated->bindInt(':languages_id', $OSCOM_Language->getId());
            $Qrelated->execute();
            
            $products_array[] = array('id' => '',
                                        'text' => $OSCOM_Test->getDef('app_test_admin_show_all'));
                                      
            while ($Qrelated->fetch()) {
                $model = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True')? $Qrelated->value('products_model') . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR : '' ;
                  $name = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True')? $Qrelated->value('products_name') : '' ;
                  $products_array[] = array('id' => $Qrelated->value('products_id'),
                                      'text' => $model . $name );
        }
        
        echo '<label for="products_id_master">Select Product:</label>' .
                    HTML::selectField('products_id_view', $products_array, (isset($_GET['products_id_view']) ? $_GET['products_id_view'] : ''), 'onchange="document.location.href=\'' . $OSCOM_Test->link("Admin&products_id_view='+this.value+'", "&products_id_view='+this.value+'").'\'"');
?>
</div>
<table class="oscom-table table table-hover">
  <thead>
    <tr class="info">
      <th class="text-left"><?php echo $OSCOM_Test->getDef('app_test_admin_table_heading_rel_id'); ?></th>
      <th class="text-left"><?php echo ((OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True') ? $OSCOM_Test->getDef('app_test_admin_table_heading_model') . ' ' . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR . ' ' : '') . ((OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') ? $OSCOM_Test->getDef('app_test_admin_table_heading_product') : '') . ' ' . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR . ' ' . $OSCOM_Test->getDef('app_test_admin_table_heading_related'); ?></th>
      <th class="text-left"><?php echo ((OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True') ? $OSCOM_Test->getDef('app_test_admin_table_heading_model') . ' ' . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR . ' ' : '') . ((OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') ? $OSCOM_Test->getDef('app_test_admin_table_heading_product') : ''); ?></th>
      <th class="text-left"><?php echo $OSCOM_Test->getDef('app_test_admin_table_heading_order'); ?></th>
      <th class="action"><?php echo $OSCOM_Test->getDef('app_test_admin_table_heading_action'); ?></th>
    </tr>
  </thead>
  <tbody>
 
    <?php
    $related_query = "select SQL_CALC_FOUND_ROWS pa.*, pd.products_id, p.products_model
                        from :table_products_related_products pa
                        left join :table_products_description pd
                        on pa.pop_products_id_master = pd.products_id
                        and pd.language_id = :languages_id
                        left join :table_products p
                        on p.products_id = pd.products_id";


         if ($products_id_view) {
              $related_query .= " where pd.products_id = '$products_id_view'";
        }
              $related_query .= "$MenuOrderBy, pa.pop_order_id, pa.pop_id";
            $related_query .= " limit :page_set_offset, :page_set_max_results";
    $Qrelated = $OSCOM_Db->prepare($related_query);
    $Qrelated->bindInt(':languages_id', $OSCOM_Language->getId());
    $Qrelated->setPageSet('20');
    $Qrelated->execute();
    

    $next_id = 1;

      $rows = 0;
      $mId = null;
      $sId = null;
      $mModel = null;
      $sModel = null;                  
      while ($Qrelated->fetch()) {
          if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True') {
            $mModel = $OSCOM_Test->osc_get_products_model($Qrelated->valueInt('pop_products_id_master')) . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR . ' ';
            $sModel = $OSCOM_Test->osc_get_products_model($Qrelated->valueInt('pop_products_id_slave')) . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR . ' ';
          }
          if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') {
            $products_name_master = tep_get_products_name($Qrelated->valueInt('pop_products_id_master'));
            $products_name_slave = tep_get_products_name($Qrelated->valueInt('pop_products_id_slave'));
          }

          $pop_order_id = $Qrelated->valueInt('pop_order_id');
          $rows++;
    
    ?>
    <tr>
      <td>&nbsp;<?php echo $Qrelated->valueInt("pop_id"); ?>&nbsp;</td>
      <td>&nbsp;<?php echo $mId  ?><?php echo $mModel ?><?php echo (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH== '0')?$products_name_master:substr($products_name_master, 0, OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH); ?>&nbsp;</td>
      <td>&nbsp;<?php echo $sId  ?><?php echo $sModel ?><?php echo (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH== '0')?$products_name_slave:substr($products_name_slave, 0, OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_DISPLAY_LENGTH); ?>&nbsp;</td>
      <td align="center">&nbsp;<?php echo $pop_order_id; ?>&nbsp;</td>
      <td align="center" class="smallText"></td></tr>
    <?php
    }
    ?>
  </tbody>
</table>

<table class="oscom-table table table-hover">
  <tr>
   <td>
    <form class="form-inline" name="relatedDropdown" action="<?= $OSCOM_Test->link('Admin&Insert'); ?>" method="post">
        <div class="form-group col-sm-4">
          <label for="products_id_master">Select master:</label>
            <select class="form-control" name="products_id_master">                                    
            <?php
                $related_what = "p.products_id";                
                $related_from =  " from :table_products p";
                $related_where =  " where p.products_id > '0'";
                if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True') {
                    $related_what .= ", p.products_model";
                }
                if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') {
                    $related_what .= ", pd.products_name";
                    $related_from .=  ", :table_products_description pd";
                    $related_where .=  " and p.products_id = pd.products_id";
                    $related_where .=  " and pd.language_id = :languages_id";
                }
                
                $related_query_raw = "select distinct " . $related_what . $related_from . $related_where . ' ' . $MenuOrderBy;
                                    
                $Qrelated_select = $OSCOM_Test->db->prepare($related_query_raw);
                $Qrelated_select->bindInt(':languages_id', $OSCOM_Language->getId());
                $Qrelated_select->execute();

                if (!$products_id_master) {
                    $products_id_master = $products_id_view;
                }
                

                while ($Qrelated_select->fetch()) {    
                    $model = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True')?$Qrelated_select->value('products_model') . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR:'';
                    $name = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True')?$Qrelated_select->value('products_name'):'';
                    $product_name = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH == '0')?$name:substr($name, 0, OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH);
                    echo '<option id="c' . $Qrelated_select->value('products_id') . '" value="' . $Qrelated_select->valueInt('products_id') . '"' . (($products_id_master == $Qrelated_select->valueInt('products_id'))? ' selected' : '') . '>' . $model . $product_name . '</option>';
                                            
                }
?>
            </select>
        </div>
        <div class="form-group col-sm-4">
          <label for="products_id_slave">Select slave:</label>
            <select class="form-control" name="products_id_slave">
            <?php
                $related_what = "p.products_id";                
                $related_from =  " from :table_products p";
                $related_where =  " where p.products_id > '0'";
                if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True') {
                    $related_what .= ", p.products_model";
                }
                if (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True') {
                    $related_what .= ", pd.products_name";
                    $related_from .=  ", :table_products_description pd";
                    $related_where .=  " and p.products_id = pd.products_id";
                    $related_where .=  " and pd.language_id = :languages_id";
                }
                
                $related_query_raw = "select distinct " . $related_what . $related_from . $related_where . ' ' . $MenuOrderBy;
                $Qrelated_select = $OSCOM_Test->db->prepare($related_query_raw);
                $Qrelated_select->bindInt(':languages_id', $OSCOM_Language->getId());
                $Qrelated_select->execute();
                                        
                while ($Qrelated_select->fetch()) {
                    $model = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_MODEL == 'True')?$Qrelated_select->value('products_model') . OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MODEL_SEPARATOR:'';
                    $name = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_USE_NAME == 'True')?$Qrelated_select->value('products_name'):'';
                    $product_name = (OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH == '0')?$name:substr($name, 0, OSCOM_APP_TEST_TEST_RELATED_PRODUCTS_MAX_NAME_LENGTH);
                    echo '<option id="d' . $Qrelated_select->value('products_id') . '" value="' . $Qrelated_select->value('products_id') . '"' . (($products_id_slave == $Qrelated_select->value('products_id'))? ' selected' : '') . '>' . $model . $product_name . '</option>';
                }
            ?>
            </select>
          </div>
          <div><?= HTML::button($OSCOM_Test->getDef('button_save'), null, null, null, 'btn-success'); ?></div>
    </form>
   </td>
  </tr>
</table>

<div>
  <span class="pull-right"><?= $Qrelated->getPageSetLinks(); ?></span>
  <?= $Qrelated->getPageSetLabel($OSCOM_Test->getDef('app_text_display_number_of_related')); ?>
</div>


<?php
  } else { //App is not installed
      $OSCOM_MessageStack->add($OSCOM_Test->getDef('alert_app_not_installed'), 'warning', 'Test');
  }

  require(__DIR__ . '/template_bottom.php');
?>

Now we need the language definitions file. Create OSC/Apps/Test/Test/languages/english/admin/admin.txt.

This is the content:

 

app_test_admin_heading_title = Related Products
app_test_admin_table_heading_model = Model
app_test_admin_select_product = Select Product
app_test_admin_show_all = Show All
app_test_admin_table_heading_rel_id = ID
app_test_admin_table_heading_product = Product
app_test_admin_table_heading_related = Related
app_test_admin_table_heading_order = Sort Order
app_test_admin_table_heading_action = Action
app_test_admin_table_heading_master_products = Master Product
app_test_admin_table_heading_slave_products = Slave Product
app_test_admin_button_insert = Insert
app_test_admin_button_reciprocate = Reciprocate
app_test_admin_button_inherit = Inherit
app_test_admin_title_maintenance = Maintenance
app_test_admin_title_maintenance_desc = Delete orphan products from related products table.
app_test_admin_button_maintenance = Delete orphans
app_test_admin_delete = Delete
app_test_admin_edit = Edit
app_test_admin_confirm_delete = Are you sure you wish to delete this item?
app_test_button_options = Options
alert_product_added_success = Added new related product
alert_product_added_failure = These products are already related
app_text_display_number_of_related = Displaying <strong>{{listing_from}}</strong> to <strong>{{listing_to}}</strong> (of <strong>{{listing_total}}</strong> related products)
alert_app_not_installed = This app isn't installed yet.

That's nearly it for today.

Note: This is a very basic version of a Related Products administration page, but it works fine for our testing purposes (plus it makes the file less complicated in case someone uses this Admin section to create a new app). In the future we'll upgrade this admin page to full functionality.

You may notice in the admin.php template file that we are using a new function which is called with this line

$OSCOM_Test->osc_get_products_model


That function goes in the OSC/Apps/Test/Test/Test.php so it is available for use throughout the app.

Simply open Test.php and add before the last curly bracket:

   

public function osc_get_products_model($product_id) {
        
        $Qmodel = $this->db->prepare('select products_model from :table_products where products_id = :products_id');
        $Qmodel->bindInt(':products_id', (int)$product_id);
        $Qmodel->execute();
    return $Qmodel->value('products_model');
    }
    

Now go ahead and install the app by clicking on the Apps->Test App menu and choosing Install (which should be the only option)

(There is no need to install the content module yet, that will be worked on next)

Now go to the Apps->Test App menu again, there should be two menu items - Admin, and Configure.  Choose Admin. If you've followed these instructions correctly you will end up at the administration page where you can create product relationships. Try a few out. At the moment we can only relate one product to another, there is no way to edit or delete these entries but that is something we will work on down the track. At the moment I'm keeping it simple so that this app could be used as a base for your app.

 

You can also click on the Options button to see how you can change the options for the app. Not all of them connect to things yet but most do, so play around.

 

As you are also probably aware, the app is completely self contained. We've added database tables, configuration parameters, pages, functions, a menu, and a content module without touching a single line of core code or having to install SQL manually or even having to install files to various different folders. The possibilities are exciting wouldn't you agree?

 

Next I'll tidy up the content module.


Let's make things easier for new osCommerce users http://forums.oscomm...bles/?p=1718900  Getting there with osCommerce 2.4! :thumbsup:


#20   clustersolutions

clustersolutions
  • Community Sponsor
  • 453 posts
  • Real Name:Tim
  • Gender:Male
  • Location:Los Angeles

Posted 21 March 2017 - 06:15

You have it on github?