Illegal string offset in PHP function

You need to be validating a couple of parts about $filearray before you use it.

There are some good suggestions in the comments, I think a couple of robust examples would serve you well too:

Using empty()

function wp_custom_attachment() {
    wp_nonce_field( plugin_basename(__FILE__), 'wp_custom_attachment_nonce' );
    $html="<p class="description">Upload your PDF here.</p>";
    $html .= '<input id="wp_custom_attachment" name="wp_custom_attachment" size="25" type="file" value="" />';

    $filearray = get_post_meta( get_the_ID(), 'wp_custom_attachment', true );
    
    if ( ! empty( $filearray['url'] ) ) { 
         $html .= '<div><p>Current file: ' . $filearray['url'] . '</p></div>'; 
    }
    echo $html; 
}

empty() is great because it implicitly works like isset, whereby if the value isn’t set it just returns false. Another benefit of empty is that you can test for structures like array keys without worrying about whether the object is an array at all:

php > var_dump( empty( $a['foo'] ) ); // $a is undefined
php shell code:1:
bool(true)
php > var_dump( empty( $a['foo']['bar'][32] ) ); // $a is undefined
php shell code:1:
bool(true)
php > var_dump( empty( $o->foo ) ); // $o is undefined
php shell code:1:
bool(true)

Above, you can see that PHP isn’t complaining about the lack of $a or $o being undefined, it just tells us “yes that is empty” and carries on.

Using isset and is_array

function wp_custom_attachment() {
    wp_nonce_field( plugin_basename(__FILE__), 'wp_custom_attachment_nonce' );
    $html="<p class="description">Upload your PDF here.</p>";
    $html .= '<input id="wp_custom_attachment" name="wp_custom_attachment" size="25" type="file" value="" />';

    $filearray = get_post_meta( get_the_ID(), 'wp_custom_attachment', true );

    if ( is_array( $filearray) && isset( $filearray['url'] ) ) { 
         $html .= '<div><p>Current file: ' . $filearray['url'] . '</p></div>'; 
    }
    echo $html; 
}

This approach does two parts of validation – first, we ensure that $filearray is indeed an array, and then we validate that the url key exists. This is more verbose than empty, and has the added benefit of a type check. It should be noted that empty( $filearray['url'] ) will only work if $filearray is an array because the syntax dictates it, however, this check is not explicit.

A note on ternary and null-coalesce

Some people suggested using a ternary:

$this_file = ! empty( $filearray['url'] ) ? $filearray['url'] : '';

This is fine, though it’s a bit hard to grok at first. You could also use null-coalescing if your PHP version supports it:

$this_file = $filearray['url'] ?? '';

While these work fine, again, I’d avoid them until you’re more comfortable in PHP. It stinks to come back to old code a few months later to try and remember what the heck you did when your code looks like

$foo = $bar ? ( $baz ?? false ) : ( $done ?: 'Waiting' );