Override src StatesProvincesList file

Hello all,

I am trying to override the StatesProvincesList.php file in the concrete > src > Localization > Service directory. I know it isn’t as simple as recreating the path to a duplicate file in the application directory as I would with other file types, and I have read this document as well, but everything I try results in a 500 site error.

I would really appreciate it if someone could lay out the process. It is not something I imagine I will have to do very often, but for the project I am working on it is necessary.

Thank you,
Craig

What are you looking to achieve by overriding that file, out of curiosity? Sometimes it helps to know what the objective is as there might be a different path to that outcome.

Because that class is always instantiated using the container, you could override it with a bind() to a custom class.
But there are several ways of doing this and @EvanCooper is right, it depends on the objective.

Hi guys,

Thanks for the replies.

The project is an e-commerce site, and I need to limit the US states in the select menu list, as the product can currently only be shipped to 6 states.

I don’t want to mess with the code that displays the select list as it has variables applied, and I would prefer to not use JS to remove the states that don’t apply.

Craig

If you’re using Community Store you’d be better served overriding the file src\CommunityStore\Utilities\StateProvince.php
That would be safer than overriding Core files.

In a package on_start method or in an on_start event somewhere try

app()->bind(
    '\Concrete\Package\CommunityStore\Src\CommunityStore\Utilities\StateProvince',
    '\Your\Own\Custom\Class'
);

In your custom class you need to override the getstates() method.
And to make sure you override as little as possible, your custom class should extend the original one.

If you’re not using Community Store or absolutely want to override the core file, you can use the same method.

I hope this helps.

1 Like

Thank you for the help with this, mnakalay. I apologize for the delay in replying, but I was out of the country for a few days.

I did indeed build this with Community Store, so I will look into the implementation you suggested. Unfortunately, I don’t have enough knowledge working with this type of override, or with extending classes in general. Can you help me with the process? I know this is a big ask, and understand if it is beyond the scope of this forum.

It’s going to be a bit involved. Here’s a step by step with the code.

In the following steps, any time a folder I’m referencing doesn’t exist, create it.

Also be mindful of when a folder or a file name is capitalized and when it’s not.

First, in application\config\generated_overrides\community_store create a file and call it authorized_us_states.php
Inside put this code:

<?php

/**
 * -----------------------------------------------------------------------------
 * Generated 2024-09-12T09:17:49-04:00
 *
 * DO NOT EDIT THIS FILE DIRECTLY
 *
 * @item      list
 * @group     authorized_us_states
 * @namespace community_store
 * -----------------------------------------------------------------------------
 */
return [
    'list' => [
        'CA',
        'MN',
        'TX',
        'WA',
    ],
];

I added a list of US states code as an example. Follow the same pattern for the states you need.

Then in application\controllers\SinglePage create the file Checkout.php

Add the following code to it:

<?php
namespace Application\Controller\SinglePage;

use Concrete\Package\CommunityStore\Controller\SinglePage\Checkout as CsCheckout;

class Checkout extends CsCheckout
{
    public function view($guest = false)
    {
        parent::view($guest);

        $finalList = ['' => ''];
        $statelist = $this->app->make('helper/lists/states_provinces')->getStates();

        $authorizedUsStates = \Config::get('community_store::authorized_us_states.list');
        foreach ($statelist as $code => $state) {
            if (in_array($code, $authorizedUsStates)) {
                $finalList[$code] = $state;
            }
        }

        $this->set("states", $finalList);
    }
}

Next, in application\controllers\CommunityStore\Utilities create the file StateProvince.php

In it add the following code

<?php
namespace Application\Controller\CommunityStore\Utilities;

use Concrete\Package\CommunityStore\Src\CommunityStore\Utilities\StateProvince as CSStateProvince;

class StateProvince extends CSStateProvince
{
    public function getStates()
    {
        $service = $this->app->make('helper/security');
        $countryCode = $service->sanitizeString($this->request->request->get('country'));
        $selectedState = $service->sanitizeString($this->request->request->get('selectedState'));
        $type = $service->sanitizeString($this->request->request->get('type'));
        $class = empty($this->request->request->get('class')) ? 'form-control' : $service->sanitizeString($this->request->request->get('class'));
        $dataList = $this->app->make('helper/json')->decode($this->request->request->get('data'), true);
        $data = '';
        if (is_array($dataList) && count($dataList)) {
            foreach ($dataList as $name => $value) {
                $data .= ' data-' . $name . '="' . $value . '"';
            }
        }

        $requiresstate = ['US', 'AU', 'CA', 'CN', 'MX', 'MY'];

        $required = '';

        if (in_array($countryCode, $requiresstate)) {
            $required = ' required="required" ';
        }

        $ret = '';

        $list = $this->app->make('helper/lists/states_provinces')->getStateProvinceArray($countryCode);
        if ($list) {
            $class = trim(str_replace('form-select', '', $class));
            $class .= ' form-select';

            if ("tax" == $type) {
                $ret .= "<select name='taxState' id='taxState' class='{$class}'{$data}>";
            } else {
                $ret .= "<select $required name='store-checkout-{$type}-state' id='store-checkout-{$type}-state' ccm-passed-value='' class='{$class}'{$data}>";
            }

            $ret .= '<option {selected} value=""></option>';
            $hasSelectedState = false;

            // Here is your list of authorized US states, modify it to suit your needs following the template
            $authorizedUsStates = \Config::get('community_store::authorized_us_states.list'); //['CA', 'WA', 'MN'];

            foreach ($list as $code => $state) {
                if ($countryCode == 'US' && !in_array($code, $authorizedUsStates)) {
                    continue;
                }
                if (!empty($selectedState) && $code == $selectedState) {
                    $ret .= "<option selected value='{$code}'>{$state}</option>";
                    $hasSelectedState = true;
                } else {
                    $ret .= "<option value='{$code}'>{$state}</option>";
                }
            }

            $ret .= "</select>";

            if ($hasSelectedState) {
                $ret = str_replace('{selected}', '', $ret);
            } else {
                $ret = str_replace('{selected}', 'selected', $ret);
            }
        } else {
            $class = trim(str_replace('form-select', '', $class));

            if (!$class) {
                $class = 'form-control';
            }

            if ("tax" == $type) {
                $ret .= "<input type='text' name='taxState' id='taxState' class='{$class}'{$data}  value='$selectedState'>";
            } else {
                $ret .= "<input type='text' name='store-checkout-{$type}-state' id='store-checkout-{$type}-state' value='{$selectedState}' class='{$class}'{$data} placeholder='" . t('State / Province') . "'>";
            }
        }

        echo $ret;

        exit();
    }
}

And finally in application\bootstrap at the bottom of the file app.php add

//Overriding Community Store StateProvince class
$app->bind(
    '\Concrete\Package\CommunityStore\Src\CommunityStore\Utilities\StateProvince',
    '\Application\Controller\CommunityStore\Utilities\StateProvince'
);

//Overriding Community Store Checkout page controller
$app->bind(
    '\Concrete\Package\CommunityStore\Controller\SinglePage\Checkout',
    '\Application\Controller\SinglePage\Checkout'
);

Now your country list, in Community Store only, should only display the states selected in the first step, when the United States are selected in the list.

I hope this helps. Feel free to reach out if something is not clear or not working.

It’s potentially quite simple.

You might just need to hook into the on_get_states_provinces_list event. The best way to do that is using a custom package. Save this code as packages/vergedesign/controller.php, and then install the package in the dashboard.

<?php

namespace Concrete\Package\Vergedesign;

use Concrete\Core\Package\Package;
use Concrete\Core\Support\Facade\Events;

class Controller extends Package {
	protected $pkgHandle = 'vergedesign';
	protected $appVersionRequired = '9.0.0';
	protected $pkgVersion = '0.1';

	public function getPackageName () {
		return t('Verge Design');
	}

	public function getPackageDescription () {
		return t('Verge Design');
	}

	public function on_start () {
		Events::addListener('on_get_states_provinces_list', function ($event) {
			$provinces = $event->getArgument('provinces');

// Either remove the states you do not want
			unset($provinces['US']['AK']);
			unset($provinces['US']['AL']);
			unset($provinces['US']['AR']);
			unset($provinces['US']['AZ']);

// or set just the ones that you do want

			$provinces['US'] = [
				'CA' => 'California',
				'MN' => 'Minnesota',
				'TX' => 'Texas',
				'WA' => 'Washington',
			];
			$event->setArgument('provinces', $provinces);
		});
	}
}

This will remove all but the states you want everywhere, and most definitely in the CS checkout.

This worked perfectly, mnakalay!

Thank you for taking the time to lay this out so clearly, and to put together the code snippets. I really appreciate it! I would not have been able to get this working without the step-by-step help.

Craig

Thank you for the reply, Jero.

I happened to get mnakalay’s reply before yours, and already implemented it. But I really appreciate that you took the time to write up the code snippet along with the directions on how to use it.

The Concrete Community is the best out there!

Craig

1 Like

@jero I think your solution will remove the states everywhere instead of just Community Store.
Would there be a way to know what package is making the call?

You’re welcome @vergedesign

@mnakalay

I have noticed an error that is popping up when I try to open the checkout page and nothing is in the cart. Normally this bounces the user to the cart page, with an alert that the cart is empty. Now I receive the following error:

Call to a member function output() on null

This error occurs on line 67 of the page: …/packages/community_store/single_pages/checkout.php

@mnakalay I did say that it would do it everywhere :wink:

I think a possible answer is to instantiate an Exception, and then inspect the call stack for evidence that the community store is in use somewhere in the stack:

<?php

namespace Concrete\Package\Vergedesign;

use Concrete\Core\Package\Package;
use Concrete\Core\Support\Facade\Events;

class Controller extends Package {
	protected $pkgHandle = 'vergedesign';
	protected $appVersionRequired = '9.0.0';
	protected $pkgVersion = '0.1';

	public function getPackageName () {
		return t('Verge Design');
	}

	public function getPackageDescription () {
		return t('Verge Design');
	}

	public function on_start () {
		Events::addListener('on_get_states_provinces_list', function ($event) {
			$e = new \Exception();
			foreach($e->getTrace() as $trace) {
				/*
				 * array (
				 * 'class' => 'Concrete\Package\CommunityStore\Controller\SinglePage\Checkout',
				 * )
				 *
				 * array (
				 * 'class' => 'Concrete\Package\CommunityStore\Src\CommunityStore\Utilities\StateProvince',
				 * )
				 */
				if (isset($trace['class']) && stripos($trace['class'], 'CommunityStore') !== false){
					$provinces = $event->getArgument('provinces');

// Either remove the states you do not want
					unset($provinces['US']['AK']);
					unset($provinces['US']['AL']);
					unset($provinces['US']['AR']);
					unset($provinces['US']['AZ']);

// or set just the ones that you do want

					$provinces['US'] = [
						'CA' => 'California',
						'MN' => 'Minnesota',
						'TX' => 'Texas',
						'WA' => 'Washington',
					];
					$event->setArgument('provinces', $provinces);

					break;
				}
			}
		});
	}
}

oh sorry @jero I read too fast. You did indeed say it :sweat_smile:

1 Like

Yes indeed, I forgot to account for redirects.

So in the file application\controllers\SinglePage\Checkout.php
replace this line:

parent::view($guest);

with this

$ret = parent::view($guest);

if (is_a($ret, 'RedirectResponse')) {
    return $ret;
}

Are you sure you don’t want to give @jero solution a go? It’s probably the safest because you’ll never have to worry about changes in Community Store or Concrete.

Thank you for the fix, @mnakalay. It looks like it is working perfectly.

I may switch over to @jero’s solution, as I don’t believe there will be situations where I need the full state list elsewhere on the site.

Again, many thanks to you both for taking the time to help out with this!

1 Like

His last solution should take care of only impacting community store.

Right, I see that @jero updated his code. Yes, I will most likely switch to this approach.