1
\$\begingroup\$

Would there be a more clear/efficient way to write

public function getModelName()
{
    return substr(strtolower(substr(strrchr(preg_replace('/(?<!\ )[A-Z]/', '_$0', __CLASS__), "\\"), 1)), 1);
}

for example:

__CLASS__: Namespace\To\SettingOption

Output: setting_option

\$\endgroup\$
  • \$\begingroup\$ Your question is a bit terse, and given your example the code doesn't work. Could you give a real example? What does __CLASS__ contain and what is the output? What if there's no namespace? \$\endgroup\$ – KIKO Software Jul 10 at 8:13
  • \$\begingroup\$ @KIKOSoftware __CLASS__ would contain the name of the class, as this method is in a trait, it would depend on the class it's used in. The class is always in a namespace, so accounting for it not being in one is not important. \$\endgroup\$ – Grey Jul 10 at 9:14
  • \$\begingroup\$ Can you provide a correct class name example? In other words: What will be contained in __CLASS__? \$\endgroup\$ – KIKO Software Jul 10 at 10:21
  • \$\begingroup\$ @KIKOSoftware edited \$\endgroup\$ – Grey Jul 10 at 10:58
3
\$\begingroup\$

You tagged laravel, so this answer assumes you are using Laravel.

Laravel has two built in helper functions that will help you do this. class_basename() will return the name of the class with the namespace stripped off, and snake_case() will convert a string to snake case.

Combine them, and you get:

public function getModelName()
{
    return snake_case(class_basename(__CLASS__));
}
\$\endgroup\$
  • \$\begingroup\$ I didn't know about snake_case(). This looks like the cleanest solution. \$\endgroup\$ – mickmackusa Jul 14 at 11:30
  • \$\begingroup\$ this is the best answer for my usecase, it's clear to read, uses logic that's already there, and it's short. \$\endgroup\$ – Grey Jul 17 at 8:10
1
\$\begingroup\$

There's actually not much I can do when it comes to efficiency, but it is clear that the code is not very readable. All these nested function calls make it very hard to track what's going on.

My suggestion would be to split this single method into multiple methods. This is a very normal strategy in programming. A method should only do one thing, and do it well. This is also called the Single responsibility principle, and although this is usually applied to classes it can apply to methods as well.

Your code could look something like this:

public function getClassName()
{
    // return class name only, without namespace, which must be present
    return substr(__CLASS__, strrpos(__CLASS__, '\\') + 1);
}

public function getClassNameWords()
{
    // return an array of the words in the camel case class name
    return preg_split('/(?=[A-Z])/', lcfirst($this->getClassName()));
}

public function getModelName()
{
    // return the name for the model as defined by the class name
    return strtolower(implode('_', $this->getClassNameWords()));
}

Each of these three functions is clearly shorter and easier to read. The extra methods allow you to reuse the code they contain. The code is no longer locked up in a single method.

\$\endgroup\$
1
\$\begingroup\$

I won't go down the rabbit hole of debating what constitutes a "valid Pascal/Studly-cased string" but there is plenty of debate here if you want to read about fringe cases (like acronyms and multibyte characters). I will merely cross my fingers and hope that your project's naming convention does not wonder into tricky territory. Either way, the post-namespace substring portion of my pattern can be easily amended to suit your input and desired output.

Effectively, you need to:

  1. Consume the namespace portion and omit it. My pattern matches zero or more sequences of not \\ then \\.
  2. Then it matches a single uppercase letter followed by zero or more lowercase letters. (not very tricky)
  3. Inside of the custom callback function, I check if there is a namespace substring matched ($m[1]), if not I prepend an underscore.
  4. Finally, I convert the first letter of the matched classname word to lowercase and return the built string as the replacement value.

PHP (Demo) (Regex Pattern Demo)

const testNamespacedClasses = [
    "Namespace\To\SettingOption",
    "I\Was\Born\InTheUSAIwas",
    "Something\Onething"
];

function testSnakeCasedModelNames() {
    foreach (testNamespacedClasses as $class) {
        echo "$class => ";
        echo  preg_replace_callback(
            '~([^\\\\]*\\\\)*([A-Z][a-z]*)~',
            function($m) {
                return ($m[1] ? '' : '_') . lcfirst($m[2]);    
            },
            $class
        );
        echo "\n---\n";
    }
}

echo testSnakeCasedModelNames();

Output:

Namespace\To\SettingOption => setting_option
---
I\Was\Born\InTheUSAIwas => in_the_u_s_a_iwas
---
Something\Onething => onething
---

Alternative snippets might look like: https://3v4l.org/lXdbd or https://3v4l.org/MMFni or https://3v4l.org/ITjTk

To implement in your project:

public function getSnakeCasedModelName()
{
    return preg_replace_callback(
        '~([^\\\\]*\\\\)*([A-Z][a-z]*)~',
        function($m) {
            return ($m[1] ? '' : '_') . lcfirst($m[2]);    
        },
        __CLASS__
    );
}

I considered using the \G metacharacter on this answer, but felt it overcomplicated the pattern and the desired result can be obtained without it

When comparing KIKO's answer versus mine, consider if "abstraction" is beneficial to your project. It is completely plausible that it is. On the other hand if you don't want to break the string manipulation process into its most basic parts, you can avoid the overhead of subsequent method calls. "Premature Abstraction" is a thing. It's an academic point, but you should consider the impacts of either approach on maintainability, testing, overhead, code size, readability, etc. I don't mean to say that there is anything wrong with KIKO's advice -- "single responsibility" is an important principle in writing professional-grade code.

Finally, because I am using a preg_ call to reformat the class string anyhow, I prefer to also use it to to remove the namespace. There are some benchmarks provided here which include a Reflection-based technique, but those benchmarks are only half of the job that you require.

If you are using __CLASS__ then I assume that you can also use __NAMESPACE__ which means that perhaps str_replace(__NAMESPACE__ . '\\', __CLASS__) can be used to trim the namespace substring from the class. Since you are using Laravel, you can use its helper function: $baseClass = class_basename($className); ...then you only need to convert StudlyCase to snake_case.

\$\endgroup\$
1
\$\begingroup\$

Efficiency & Clarity

For efficiency the only "flaws" (it's more for "good habit" than noticeable optimization - it doesn't matter for such a small strings) would be the order of functions (regexp is costly so it should be called on string processed as far as possible) and regexp itself which could be optimized for least amount of steps.

When it comes to clarity it's enough to split those function calls into named variables, so you could later read what steps you're doing here. It can be later split into separate methods, but there's no need since method stays small (style) and IS responsible for one thing (see below).

public function getModelName(): string
{
    $className = substr(strrchr(__CLASS__, "\\"), 1);
    $camelCase = lcfirst($className);
    $snakeCase = strtolower(preg_replace('/[A-Z]/', '_$0', $camelCase));

    return $snakeCase; //seems redundant, but don't be afraid to do that
                       //when it helps in readability
                       //it's more important than that 1ms/req
}

Things to consider

  • This method may be static - it doesn't rely on object fields, but refers to the class, and static scope denotes that. It still might be called using instance identifier: $object::getModelName();
  • You're dealing with meta programming magic here - unless it's not some dev tool your first thought should be don't do it
  • SRP that @KIKOSoftware brought up is often misunderstood (vaugue naming), and it's not about doing one thing inside the class/method (that's hardly possible), but upon calling a method from the outside - responsibility towards client that expects one concrete thing (coherent group of things). You're breaking this principle anyway since your "model name" is not even an instance method and it represents some meta data next to, I persume, other methods of the class (it's not important when this class is only a data structure, but you will be coupled to convention here instead of abstraction).
\$\endgroup\$
1
\$\begingroup\$

Subclassing

What's the point of this code? Since __CLASS__ always refers to the class in which the code appears, rather than the class of the current object, this method doesn't work with subclassing. That is, with the following code, (new SubModel())->getModelName() would produce base_model, not sub_model:

class BaseModel {
    public function getModelName() {
        return substr(strtolower(substr(strrchr(preg_replace('/(?<!\ )[A-Z]/', '_$0', get_class($this)), "\\"), 1)), 1);
    }
}

class SubModel extends BaseModel {
}

If that's the case, why not just hard-code the result of getModelName() rather than using reflection?

Alternatively, if you want to make the code work with subclassing, use get_class($this) instead of __CLASS__.

Deciphering

That one-liner is hard to read because it does too many things. One problem is that it's hard to see which argument goes with which function call. Maybe indentation could help?

public function getModelName()
{
    return substr(
        strtolower(
            substr(
                strrchr(
                    preg_replace('/(?<!\ )[A-Z]/', '_$0', __CLASS__),
                    "\\"
                ),
                1
            )
        ),
    1);
}

But even that is ridiculously hard to read, because there are no hints as to what each part of the code does. I don't know why there is a negative look-behind assertion in the regex. The outermost substr(…, 1) should be avoidable if the preg_replace(…) call were smarter. Maybe comments could help make it more decipherable?

Suggested solution

I suggest writing it as three statements:

  1. Get the name of the class, and discard the namespace (if any — your code doesn't handle un-namespaced classes).
  2. Find all the CamelCase components within the name.
  3. Join them with _ and make the result lowercase.
public function getModelName() {
    $unqual_class = end(explode('\\', get_class($this)));
    preg_match_all('/.[^A-Z]*/', $unqual_class, $matches);
    return strtolower(implode('_', $matches[0]));
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.